Skip to main content

leaf_core/
wysiwyg.rs

1//! The WYSIWYG view: render the document with its markup *resolved*, not shown —
2//! headings and code tagged with a typographic role (a frontend sizes or colours
3//! them; see [`crate::style`]), `**bold**` as real bold, `# ` / `**` / `` ` ``
4//! delimiters hidden — while keeping every visible glyph tied back to the source
5//! byte it came from.
6//!
7//! That back-reference (`Glyph::src`) is what lets a caret still work: the caret
8//! stays a source offset (shared with the source view), but the [`VisualMap`]
9//! converts between an offset and a screen `(row, col)`, so cursor drawing,
10//! mouse clicks, and vertical motion all operate in *visible* space.
11//!
12//! Left and Right instead walk the map's caret *stops* in document order. On
13//! ordinary prose that's the same journey — the stops are laid out left to right
14//! — and it steps over the hidden delimiters either way. They part company only
15//! in a table, where the text is arranged in two dimensions and a cell wrapped
16//! within its column continues *below* rather than to the right. Following the
17//! document is what a caret means there.
18//!
19//! Text is walked from the AST (`str` nodes carry exact spans, and their text is
20//! the verbatim source slice), so a Markdown and a Djot file that parse alike
21//! render — and map — identically.
22
23use std::cell::{Cell, RefCell};
24use std::collections::HashMap;
25use std::ops::Range;
26use std::sync::OnceLock;
27
28use twig::{Alignment, ContainerOrigin, DirectiveForm, Editor, FlatNode, Kind, QueryMatch};
29use unicode_segmentation::UnicodeSegmentation;
30use unicode_width::UnicodeWidthStr;
31
32use crate::style::{
33    Align, Baseline, FaceId, FaceRef, FaceTable, FontSize, LineHeight, MarkColor, Role, Style,
34    TextColor, Token,
35};
36
37/// One rendered character plus the source byte offset it originates from.
38/// Synthetic glyphs (a list bullet, a quote gutter) point at their block's
39/// start, so clicking one lands the caret at the start of that block.
40#[derive(Clone)]
41pub struct Glyph {
42    pub ch: char,
43    pub style: Style,
44    pub src: usize,
45    /// Whether the caret may *rest* on this glyph. Decoration — a table border
46    /// or a cell's alignment padding — is visible but isn't text, so the caret
47    /// steps over it instead of into it. It also can't be a stop even in
48    /// principle: a run of decoration shares one `src`, and a caret can only
49    /// move by changing offset, so resting on it would pin horizontal motion.
50    /// A click still maps through `src`, which is why decoration points at the
51    /// text it decorates.
52    ///
53    /// Real text is a stop once per *grapheme cluster*, on the glyph that opens
54    /// it: the continuation glyphs of an emoji or an accented letter are drawn,
55    /// but standing between them is standing inside a character.
56    pub stop: bool,
57}
58
59impl Glyph {
60    /// The character to draw: [`ch`](Self::ch), except a list item's indent
61    /// on its later rows ([`Role::ListIndent`]), which is spelled with the
62    /// marker's characters for its width and drawn blank.
63    pub fn drawn(&self) -> char {
64        if self.style.role == Role::ListIndent {
65            ' '
66        } else {
67            self.ch
68        }
69    }
70}
71
72/// One visual line. `end_src` is the source offset a caret sits at when placed
73/// at the line's end (past its last glyph) — the anchor for end-of-line and
74/// click-past-content.
75///
76/// `Clone` so a block's rows can be cached and re-emitted at a shifted offset
77/// across an edit — see [`BlockCache`].
78#[derive(Clone)]
79pub struct VRow {
80    pub glyphs: Vec<Glyph>,
81    pub end_src: usize,
82    /// A row that is drawn but holds no caret: a table's `├───┼───┤` rules, and
83    /// the blank gap a block boundary is spelled with. Vertical motion steps
84    /// over it, `pos_of_offset` never resolves onto it, and its stops (it has
85    /// none) and `end_src` stay out of the map's stop table.
86    ///
87    /// Emptiness isn't the test — an empty paragraph is a blank row too, and a
88    /// real caret stop. The test is whether the row is somewhere text can go.
89    pub decoration: bool,
90    /// This row is one line of a fenced or indented code block. Set on every row
91    /// the `"code_block"` arm emits — including its blank lines, which carry no
92    /// glyph to tell them apart otherwise. A frontend draws its own chrome (a
93    /// border and a tinted background) around each maximal run of these, and
94    /// scrolls them horizontally instead of wrapping; see
95    /// [`VisualMap::code_blocks`]. Survives the row shuffling of [`BlockCache`]
96    /// reuse and [`build_spliced`] because it rides on the row, not on a
97    /// row-index span the way a table's picture does.
98    pub code: bool,
99    /// A fenced code block's info string (its language), carried on the *first*
100    /// row of the block so it survives row reuse the way [`code`](Self::code)
101    /// does. `None` on every other row, and on an indented block (which has no
102    /// fence to label). A frontend paints it as a small label on the block's box
103    /// and edits it through a prompt — see [`CodeBlockInfo::lang`]. It's a plain
104    /// display string, not a source slice, so it needs no offset shifting; the
105    /// label re-derives from twig on the next build.
106    pub code_lang: Option<String>,
107    /// This row belongs to a `:::name{.class}` directive container — twig's
108    /// generic fenced-div block, whose meaning is entirely up to the host app
109    /// (diaryx's `:::vis{.audience}` visibility blocks, say). Set on every row
110    /// the `"directive"` arm emits, the same way [`code`](Self::code) marks a
111    /// code block's rows, so a frontend can draw a tinted panel around each
112    /// maximal run of these.
113    pub directive: bool,
114    /// A directive container's space-joined attrs — dot-prefixed classes
115    /// (`.public .family` → `"public family"`) unioned with bare pandoc-style
116    /// words (`public family`, no leading dot — diaryx's other `:::vis{...}`
117    /// convention), carried on the block's *first* row only — the
118    /// [`code_lang`](Self::code_lang) pattern. `None` on every other row, and
119    /// when the directive carries no such attrs. A frontend paints it as a
120    /// small label on the block's panel; it's a plain display string, not a
121    /// source slice, so it rides row reuse untouched.
122    pub directive_label: Option<String>,
123    /// Set on the single placeholder row a block-level image renders to, carrying
124    /// the image's destination and alt text; `None` on every other row. The row's
125    /// glyphs are the default `🖼 alt` label (which a plain surface paints as-is);
126    /// an image-capable frontend reads this to paint the real picture instead,
127    /// skipping the row named by [`MediaInfo::rows_span`]. Like
128    /// [`code_lang`](Self::code_lang) it's plain display strings, not source
129    /// slices, so it rides row reuse and needs no offset shifting; the map's
130    /// [`media`](VisualMap::media) side-table is derived from it once the rows
131    /// are final, the same way [`code_blocks`](VisualMap::code_blocks) is.
132    pub media: Option<MediaMark>,
133    /// Set on the **first** row of a task list item, carrying whether its box is
134    /// ticked; `None` on every other row, including a plain `list_item`'s. The
135    /// row's glyphs already draw the box as `☐ `/`☑ ` in the marker's place, so a
136    /// plain surface needs nothing further; a GUI reads this to paint a real
137    /// checkbox widget and to know which way it is facing.
138    ///
139    /// A `bool` rather than a source span, for the reason
140    /// [`code_lang`](Self::code_lang) is a plain string: it rides [`BlockCache`]
141    /// reuse and [`build_spliced`] untouched, needing no offset shifting. To
142    /// *toggle* the box, a frontend maps its click to a source offset the way it
143    /// maps any other — the marker's glyphs carry the item's own `src` — and
144    /// hands that to [`crate::Doc::toggle_task_at`].
145    pub task: Option<bool>,
146    /// Set on the single placeholder row a **leaf** directive (`::name{…}`)
147    /// renders to, carrying its name and attributes; `None` on every other row.
148    /// The container form isn't this — it wraps real blocks and marks each of
149    /// them [`directive`](Self::directive) instead. Like [`media`](Self::media)
150    /// it's plain display strings, so it rides row reuse untouched, and the map's
151    /// [`directives`](VisualMap::directives) side-table is derived from it once
152    /// the rows are final.
153    pub leaf_directive: Option<DirectiveMark>,
154    /// The heading level (1–6) of the block this row belongs to, on every row a
155    /// `heading` emits (a long one wraps to several) and `None` everywhere else.
156    ///
157    /// A frontend that sizes a whole line — a proportional renderer giving the
158    /// row a bigger line box — needs the level *per row*, and the glyphs can't
159    /// always supply it: an empty heading (`# ` with nothing typed after it,
160    /// which is what the toolbar's H1 leaves on a blank line) has no glyph to
161    /// carry a [`Role::Heading`] at all, so a glyph scan called it body text and
162    /// the line drew at body height until the first character landed. Riding the
163    /// row says it once, for the empty case and the wrapped case alike.
164    ///
165    /// Per-*glyph* styling still comes from [`Role::Heading`] on the glyphs; this
166    /// is the row-level fact, and the two agree wherever a heading has content —
167    /// same `u8` level, clamped the same way [`heading_style`] clamps it.
168    pub heading: Option<u8>,
169    /// How this row's block is aligned across the measure — the author's
170    /// `class="center"`, on every row the block emits and `None` for the
171    /// theme's default, which is left.
172    ///
173    /// A *row* fact and not a glyph one for [`heading`](Self::heading)'s reason,
174    /// and more sharply: alignment is a property of the *line*, not of the
175    /// letters on it, so an empty paragraph the author has just centred has to
176    /// carry it with no glyph to hang it on. It rides the row like a plain
177    /// `Copy` flag, so [`BlockCache`] reuse and [`build_spliced`] carry it
178    /// untouched.
179    ///
180    /// Read from the paragraph's or heading's own attributes and from those of
181    /// every `div` around it, the nearest winning — so `<div class="center">`
182    /// around three paragraphs centres all three, which is what the author of
183    /// that HTML meant.
184    pub align: Option<Align>,
185    /// How far apart this row's block sets its lines, as a multiple of the
186    /// theme's own line height — the author's `data-line-height`, on every row
187    /// the block emits and `None` for the theme's spacing.
188    ///
189    /// A frontend that lays rows out in pixels scales the row's height by
190    /// [`LineHeight::as_f32`]; one that draws a row per terminal line ignores
191    /// it, the way it ignores a heading's size. Read at the same two levels
192    /// [`align`](Self::align) is, and one of the menu's three names or the
193    /// exact ratio the author asked for.
194    pub line_height: Option<LineHeight>,
195    /// What this row divides, on the blank rows a block boundary is *drawn* with
196    /// and `None` on every other row — including the navigable blank lines of
197    /// preserve-soft flow, which are somewhere text can go rather than a gap
198    /// between blocks. So `boundary.is_some()` is exactly "this row is a drawn
199    /// block boundary", the [`decoration`](Self::decoration) rows that come from
200    /// [`Builder::emit_separators_before`].
201    ///
202    /// It exists because a boundary's *height* is a frontend decision but its
203    /// *kind* is not. Typography spaces a boundary by what it separates — the
204    /// margin above a heading is wider than the one between two paragraphs, so
205    /// the heading groups with the text it introduces — and a frontend that has
206    /// only rows to look at has to re-derive the structure by sniffing glyph
207    /// roles. Three frontends sniffing separately is three chances to disagree
208    /// about the same document. Core already knows, having just walked the AST
209    /// to emit this row, so it says so once here and each frontend multiplies by
210    /// its own spacing.
211    pub boundary: Option<Boundary>,
212    /// The offsets on this row where an inline mark's *content* ends under a
213    /// hidden closing delimiter — the end of the `d` in `**bold**`, one byte
214    /// before the `**` that draws nothing. Each is a caret stop with no glyph
215    /// of its own: the caret standing there is drawn where the next glyph is,
216    /// but typing there extends the mark, where typing past the delimiter
217    /// leaves it. See [`VisualMap::mark_ends`] for the rule.
218    ///
219    /// Source offsets, so [`shift_row`] moves them with the glyphs; empty on
220    /// decoration rows and on every row no mark closes on.
221    pub mark_ends: Vec<usize>,
222    /// The formulas this row stands in for, in glyph order: one [`MathMark`]
223    /// per inline atom on the row, or the single block mark on the
224    /// placeholder row a display formula renders to. Empty on every other
225    /// row, and on the revealed line, where a formula is its TeX and no
226    /// picture stands for it.
227    ///
228    /// Plain strings and a glyph *index* rather than a source offset, for
229    /// [`media`](Self::media)'s reason: the mark rides [`BlockCache`] reuse and
230    /// [`build_spliced`] untouched, and the glyph it names carries the offset.
231    /// The map's [`math`](VisualMap::math) side-table is derived from these
232    /// once the rows are final.
233    pub math: Vec<MathMark>,
234}
235
236/// One formula a row stands in for — what a picture-capable frontend typesets
237/// and draws in place of the glyph or the rows that hold its spot. See
238/// [`VRow::math`] and, for the frontend's view of the same thing,
239/// [`MathInfo`].
240#[derive(Clone, Debug, PartialEq, Eq)]
241pub struct MathMark {
242    /// The TeX between the delimiters, verbatim — a display block's keeps its
243    /// newlines. What `leaf-math` typesets.
244    pub tex: String,
245    /// Display style (`$$…$$`) rather than text style (`$…$`). True for every
246    /// block mark, and for a `$$…$$` written inside a line of prose, which is
247    /// display style set inline.
248    pub display: bool,
249    /// The index into [`VRow::glyphs`] of the atom this mark stands behind —
250    /// the one [`Role::Math`] glyph an inline formula renders to. `None` for
251    /// a block mark, whose placeholder is the whole row.
252    pub glyph: Option<usize>,
253    /// How many rows a block formula reserves — the label row plus the blank
254    /// fillers under it, the [`MediaMark::rows`] recipe, and from the same
255    /// door: a terminal frontend that has typeset and measured the picture
256    /// reports its height through [`crate::Doc::set_math_rows`]. `1` for an
257    /// inline atom, which reserves nothing.
258    pub rows: usize,
259}
260
261/// What a drawn block boundary separates: the kinds of the blocks it falls
262/// between — the pair a frontend spaces by.
263#[derive(Clone, Copy, Debug, PartialEq, Eq)]
264pub struct Boundary {
265    pub above: BlockClass,
266    pub below: BlockClass,
267}
268
269/// The block kinds core tells apart when it walks a document — the vocabulary
270/// [`Boundary`] is spelled in. A statement about *structure*, not about how any
271/// of it should look: what a frontend does with "this gap sits above a heading"
272/// is entirely the frontend's.
273///
274/// `Class` rather than `Kind` because [`twig::BlockKind`] already means
275/// something else in this crate's public surface — the *command* vocabulary
276/// (`Paragraph | Heading(n)`) a toolbar passes to [`Doc::set_block`](crate::Doc::set_block).
277/// This is the reverse direction: what a block already *is*, read back off a
278/// rendered row.
279///
280/// [`BlockClass::Other`] is the honest answer for a node kind core doesn't
281/// separate out, so adding one here is additive for every frontend: nothing has
282/// to change until it wants to space that kind differently.
283#[derive(Clone, Copy, Debug, PartialEq, Eq)]
284pub enum BlockClass {
285    Paragraph,
286    Heading,
287    /// A whole list. Its *items* are [`BlockClass::ListItem`]; note that core
288    /// draws no boundary row between two items of one list, tight or loose, so
289    /// an item↔item pair never reaches a frontend.
290    List,
291    ListItem,
292    Quote,
293    Code,
294    Table,
295    /// A block-level image, video, or audio.
296    ///
297    /// Never reached through [`from_node_kind`](BlockClass::from_node_kind): a
298    /// block picture is not a node of its own — [`Builder::media_only`] promotes
299    /// the paragraph (or `<picture>`/`<video>` container) wrapping it — so the
300    /// walk only ever sees the wrapper's kind. [`label_media_boundaries`] reads
301    /// it back off the finished rows instead, after the fact.
302    Media,
303    /// A display formula on lines of its own — a paragraph holding nothing but
304    /// a `$$…$$`. Never reached through [`from_node_kind`](BlockClass::from_node_kind)
305    /// for [`Media`](BlockClass::Media)'s reason: the walk sees the wrapping
306    /// paragraph, and [`label_media_boundaries`] relabels the gaps around the
307    /// placeholder once the rows are final.
308    Math,
309    /// A `:::name{.class}` directive container.
310    Directive,
311    Rule,
312    Footnote,
313    Other,
314}
315
316impl BlockClass {
317    /// Classify a twig node kind — the same vocabulary [`Builder::block`]
318    /// matches on, so the two can't drift about what a block is. Both the
319    /// whole-arena walk (which has [`FlatNode`]s) and the incremental top-level
320    /// walk (which has only a query match's kind) reach it by this one door.
321    pub fn from_node_kind(kind: &Kind) -> BlockClass {
322        match kind {
323            Kind::Para => BlockClass::Paragraph,
324            Kind::Heading => BlockClass::Heading,
325            Kind::BulletList | Kind::OrderedList | Kind::TaskList => BlockClass::List,
326            Kind::ListItem | Kind::TaskListItem => BlockClass::ListItem,
327            Kind::BlockQuote => BlockClass::Quote,
328            Kind::CodeBlock => BlockClass::Code,
329            Kind::Table => BlockClass::Table,
330            Kind::Image => BlockClass::Media,
331            // twig 2.8 folded `div`/`span`/`directive`/`element` into one
332            // `container` kind, so a `:::note` panel and a promoted `<video>`
333            // arrive here indistinguishable — telling them apart needs the
334            // node's `origin`, and the incremental walk has only this kind.
335            // `Directive` is the right answer for the case that motivates the
336            // class (nothing else draws a tinted panel) and a harmless one for
337            // the rest: `BlockClass` is descriptive and core never branches on
338            // it. The one case where it was actively wrong — a promoted
339            // `<video>`, which would have been handed to a frontend as something
340            // to draw a fenced-div panel around — is corrected by
341            // [`label_media_boundaries`] once the rows are final, along the same
342            // door as a block image. Anything else that must be exact reads
343            // [`container_is_directive`] off a real node.
344            Kind::Container => BlockClass::Directive,
345            Kind::ThematicBreak => BlockClass::Rule,
346            Kind::Footnote => BlockClass::Footnote,
347            _ => BlockClass::Other,
348        }
349    }
350}
351
352/// The name and attributes a leaf directive's placeholder row carries, so a
353/// frontend that knows the host app's vocabulary can paint the real thing —
354/// an embedded page for diaryx's `::embed{src=…}`, a generated table of
355/// contents for a `::toc`, and the plain `⧉ name` label for one it doesn't
356/// know. The peer of [`MediaMark`], and plain strings for the same reason: they
357/// survive the row shuffling of [`BlockCache`] reuse and [`build_spliced`].
358#[derive(Clone, Debug, PartialEq, Eq)]
359pub struct DirectiveMark {
360    /// The directive's type — `embed`, `toc`, `vis` — with no leading colons.
361    /// Core is agnostic of what it means: the vocabulary is the host app's.
362    pub name: String,
363    /// Its `{…}` attributes as `(key, value)` pairs in source order. A bare
364    /// attribute (`{public}`) has a `None` value, the way twig reports it.
365    pub attrs: Vec<(String, Option<String>)>,
366    /// The directive's `[label]` text, flattened from its inline children, or
367    /// empty when it has none. Also what the placeholder label shows.
368    pub label: String,
369    /// How many visual rows this directive reserves — the label row plus blank
370    /// filler rows below it, so a frontend painting something real has the
371    /// vertical room. `1` is the bare placeholder, and the only value core
372    /// produces today: unlike an image (whose height a terminal frontend
373    /// measures and reports back), nothing has told core how tall an embed is.
374    /// A pixel-laid-out GUI sets its own height regardless.
375    pub rows: usize,
376}
377
378/// What a block-level media placeholder actually is, so a frontend knows which
379/// widget to build over the reserved rows: a raster, a movie player, or a
380/// transport with no picture at all. Core classifies and stops there — it opens
381/// nothing, so this is a statement about the *markup*, not about a file it has
382/// verified exists or can decode.
383#[derive(Clone, Copy, Debug, PartialEq, Eq)]
384pub enum MediaKind {
385    /// A `![](…)` / `<img>` / `<picture>` — a still picture.
386    Image,
387    /// An HTML `<video>`. Markdown and Djot spell no video of their own, so this
388    /// only ever arrives through `html_elements` promotion (or a `::video{…}`
389    /// directive a host app maps itself, which core reports as a directive).
390    Video,
391    /// An HTML `<audio>` — a transport with no picture, so a frontend gives it a
392    /// fixed control height rather than measuring an aspect ratio.
393    Audio,
394}
395
396/// Which of the two caret homes a block media has — see
397/// [`VisualMap::block_media_stop`].
398#[derive(Clone, Copy, Debug, PartialEq, Eq)]
399pub enum MediaStop {
400    /// The stop in front of the picture. What is typed here belongs above it.
401    Before,
402    /// The stop just past it. What is typed here belongs below it.
403    After,
404}
405
406impl MediaKind {
407    /// The emoji a plain surface prefixes the placeholder label with — the
408    /// `🖼`/`🎬`/`🔊` that makes the row read as *a thing* rather than as text.
409    fn sigil(self) -> char {
410        match self {
411            MediaKind::Image => '🖼',
412            MediaKind::Video => '🎬',
413            MediaKind::Audio => '🔊',
414        }
415    }
416}
417
418/// The destination and label a block-level media placeholder row carries, so a
419/// capable frontend can resolve and paint the real thing. Plain strings (no
420/// source offsets), so they survive the row shuffling of [`BlockCache`] reuse
421/// and [`build_spliced`] untouched — see [`VRow::media`].
422#[derive(Clone, Debug, PartialEq, Eq)]
423pub struct MediaMark {
424    /// Whether this is a picture, a movie, or a sound — which widget the
425    /// frontend builds over the reserved rows.
426    pub kind: MediaKind,
427    /// The media's link destination — a path, URL, or `data:` URI, verbatim from
428    /// the AST. A frontend resolves a relative path against the document's
429    /// directory itself; core holds no I/O.
430    ///
431    /// Empty is possible and legal for a `<video>`/`<audio>`, which may carry no
432    /// `src` of its own and name its candidates in child `<source>`s instead —
433    /// unlike an `<img>`, whose `src` *is* the picture. A frontend with an empty
434    /// destination takes its URL from [`sources`](MediaMark::sources).
435    pub destination: String,
436    /// A `<picture>`'s theme/media alternatives, in document order, when this
437    /// block image came from one; empty for a plain `![](…)` / bare `<img>`. Each
438    /// is a `<source>`'s media query + candidate URL(s); a frontend that knows its
439    /// theme picks the first whose media matches and falls back to [`destination`]
440    /// (the `<img>`). Core keeps them verbatim and picks nothing — it has no theme.
441    ///
442    /// [`destination`]: MediaMark::destination
443    pub sources: Vec<MediaSource>,
444    /// The media's alt text (its rendered inline children, flattened), or empty
445    /// when it has none. Also what the placeholder label shows. For a `<video>`/
446    /// `<audio>` this is the element's own text content — the "your browser does
447    /// not support…" fallback, which doubles as its accessible name.
448    pub alt: String,
449    /// A `<video poster="…">`'s still frame, verbatim, or empty when there is
450    /// none (and always empty for an image or audio). It is an *image*
451    /// destination, so a frontend already able to draw a picture can show it
452    /// before the movie loads — or in place of one it can't play at all.
453    pub poster: String,
454    /// How many visual rows this media reserves — the placeholder label row plus
455    /// the blank filler rows below it, so a frontend that paints a real raster has
456    /// the vertical room to draw it. `1` is the bare placeholder (a frontend that
457    /// can't draw pictures, or an image it couldn't resolve). A terminal frontend
458    /// asks for as many rows as the fitted picture is tall; the pixel-laid-out GUI
459    /// ignores this and sets its own row height, so it always leaves it `1`. The
460    /// count comes from the frontend (via [`crate::Doc::set_media_rows`]) because
461    /// core does no I/O and can't measure the image itself. See [`VRow::media`].
462    pub rows: usize,
463}
464
465/// One `<source>` under a `<picture>`, `<video>`, or `<audio>`: a candidate URL
466/// plus whichever of the two things HTML lets a `<source>` be chosen by — a
467/// media query (`<picture>`) or a MIME type (`<video>`/`<audio>`). Verbatim from
468/// the AST: core carries the alternatives and resolves none of them, having
469/// neither a theme nor a codec list to judge them by.
470///
471/// The two spellings are normalised onto one field. `<picture>` writes
472/// `srcset`, `<video>`/`<audio>` write `src`; both land in
473/// [`srcset`](MediaSource::srcset), since a frontend wants the URL either way
474/// and only `<picture>` ever uses the descriptor syntax.
475#[derive(Clone, Debug, PartialEq, Eq)]
476pub struct MediaSource {
477    /// The `<source media="…">` query, verbatim (`"(prefers-color-scheme: dark)"`),
478    /// or empty for a `<source>` with no `media` (an unconditional override, and
479    /// the norm for `<video>`/`<audio>`, which pick by codec rather than theme).
480    pub media: String,
481    /// The candidate URL(s): a `<picture>`'s `srcset` verbatim — one URL, or a
482    /// comma-separated candidate list with `1x`/`2x`/width descriptors — or a
483    /// `<video>`/`<audio>` `<source>`'s plain `src`. A frontend takes the first
484    /// URL token; the theme and codec cases both only ever need that.
485    pub srcset: String,
486    /// The `<source type="…">` MIME type (`"video/webm"`), verbatim, or empty
487    /// when the `<source>` declares none. How a `<video>`/`<audio>` frontend
488    /// picks a candidate it can actually decode; a `<picture>`'s sources
489    /// normally leave it empty and are chosen by [`media`](MediaSource::media).
490    pub mime: String,
491}
492
493/// The rendered document plus the offset⇄position mapping the caret rides on.
494#[derive(Clone, Default)]
495pub struct VisualMap {
496    /// The document's **default monospace rendering** — one [`VRow`] of glyphs
497    /// per visual line, tables spelled with box-drawing borders (`│ ─ ┌┬┐…`) and
498    /// cells padded to whole character-cell columns. Any monospace surface can
499    /// draw these verbatim, so a consumer gets a working view for free: the TUI
500    /// paints them as-is, and a five-line plain-text dump would too.
501    ///
502    /// It's a *default*, not the only truth. A frontend with its own geometry —
503    /// a proportional GUI — lays text out in its own units, and for a table
504    /// skips the box-drawn rows named by [`TableInfo::rows_span`] and draws from
505    /// the structural [`TableInfo`] instead. The box glyphs live here rather than
506    /// in a frontend precisely because they *are* a renderable default: unlike a
507    /// colour (a role each surface must map to its own palette — see
508    /// [`crate::style`]), `┌─┐` is finished text that needs no interpretation.
509    pub rows: Vec<VRow>,
510    /// The first source offset that is actually rendered — the caret floor for
511    /// the WYSIWYG view. Non-zero when a leading `metadata` block (YAML/TOML
512    /// frontmatter) is skipped: the frontmatter is preserved in the source and
513    /// editable in the source view, but hidden and unreachable here, so the
514    /// caret and selection can't wander into it (and copy won't grab it).
515    pub content_start: usize,
516    /// Every offset the caret may rest at, ascending and deduplicated: each
517    /// row's stop glyphs plus the row's own end (the "after the last character"
518    /// spot every line needs). Decoration contributes nothing.
519    ///
520    /// Left/Right read this instead of walking the grid, because the grid isn't
521    /// laid out in offset order: a table with wrapped cells puts column 1's
522    /// second line *below* column 2's first, so "the next stop rightward" and
523    /// "the next stop in the document" part ways. Following the document is what
524    /// a caret means — and on every row that *is* in order the two agree anyway,
525    /// so nothing else has to change.
526    stops: Vec<usize>,
527    /// The caret's second home at the end of every hidden inline mark: the
528    /// offset where the mark's content ends, one byte before its closing
529    /// delimiter — ascending and deduplicated, from every row's
530    /// [`VRow::mark_ends`].
531    ///
532    /// With delimiters hidden, `**bold** tail` draws one spot after the `d`
533    /// and the source has two offsets for it: the content end (inside the
534    /// mark, where typing extends the bold) and the byte past the `**` (where
535    /// typing leaves it). Only the second is a glyph's offset, so only it was
536    /// a stop, and a caret asked to rest at the first was snapped a whole
537    /// character back onto the `d` — a drag over `bold` came back one letter
538    /// short. The delete and backspace paths already settle the caret on the
539    /// content end as its natural home there
540    /// ([`crate::Doc::settle_inside_close_delims`]); this makes it one the
541    /// caret can be placed at and step onto too.
542    ///
543    /// Kept apart from [`stops`](Self::stops) rather than merged in, because
544    /// the two lists answer different questions. A stop with no glyph is
545    /// invisible to a walk that pairs stops with characters — a system text
546    /// input counting `position(from:offset:)` steps against the text it was
547    /// shown would drift a character at every mark — and to word motion, which
548    /// classifies a stop by the source byte under it (a `*`). So
549    /// [`stop_after`](Self::stop_after) and its kin walk the glyph stops alone,
550    /// and only the places a caret *rests* — snapping, resting checks, and
551    /// Left/Right — read both.
552    mark_ends: Vec<usize>,
553    /// Every table in the document, in order, described structurally rather than
554    /// drawn — see [`TableInfo`] for why both exist.
555    pub tables: Vec<TableInfo>,
556    /// Every fenced/indented code block, in order, as the range of [`rows`] it
557    /// occupies — a frontend draws one bordered, tinted box around each and
558    /// scrolls it horizontally rather than wrapping. Derived from the per-row
559    /// [`VRow::code`] flag once the rows are final (so it survives incremental
560    /// row reuse), the same way [`collect_stops`] derives the stop table.
561    ///
562    /// [`rows`]: VisualMap::rows
563    pub code_blocks: Vec<CodeBlockInfo>,
564    /// Every block-level image in the document, in order — one per placeholder
565    /// row a frontend replaces with a real picture. Derived from the per-row
566    /// [`VRow::media`] mark once the rows are final (so it survives incremental
567    /// row reuse), the same way [`code_blocks`](VisualMap::code_blocks) is
568    /// derived from [`VRow::code`].
569    pub media: Vec<MediaInfo>,
570    /// Every **leaf** directive in the document, in order — one per placeholder
571    /// row a frontend may replace with whatever the host app's vocabulary makes
572    /// of it. Derived from the per-row [`VRow::leaf_directive`] mark once the
573    /// rows are final, exactly as [`media`](VisualMap::media) is.
574    pub directives: Vec<DirectiveInfo>,
575    /// Every formula standing as a picture in this map, in row order — each
576    /// inline atom and each display block's placeholder. A frontend that
577    /// paints pictures typesets each one's TeX and draws it over the glyph or
578    /// the rows named. Derived from the per-row [`VRow::math`] marks once the
579    /// rows are final, exactly as [`media`](VisualMap::media) is.
580    ///
581    /// A formula on the revealed line is not here: there it is its own TeX,
582    /// drawn as code, and nothing stands in for it.
583    pub math: Vec<MathInfo>,
584    /// Every named font family this map's glyphs are set in, by the
585    /// [`FaceId`] they carry — the side table that lets [`Style`] stay `Copy`
586    /// while a family name stays a `String`.
587    ///
588    /// Not derived from the rows the way [`code_blocks`](Self::code_blocks) is,
589    /// because the name is not on the rows: it is interned as the walker meets
590    /// the attribute. So each of the three build paths assembles it from what
591    /// it actually walked — a fresh build from its own walk, a cached build
592    /// from each block's walk or the names its cache entry stored, a splice
593    /// from the previous map's table plus the one block it re-rendered. A
594    /// [`FaceId`] is derived from the name rather than being an index, which is
595    /// what makes those three agree glyph for glyph; see the type's note.
596    faces: FaceTable,
597    /// The visible text tabulated over the stops — see [`Spelling`]. Derived
598    /// on the first lookup that asks for it rather than by the build paths,
599    /// since a map that is only ever drawn never needs it, and a splice
600    /// would otherwise have to patch it the way it patches the stops.
601    spelling: OnceLock<Spelling>,
602}
603
604/// The visible text ([`VisualMap::visible_text`]) as a table over the stops:
605/// for stop `i`, the character the text spells it with — `None` for the
606/// `'\n'` of a stop that draws no glyph — and the UTF-16 length of the text
607/// before it. A UTF-16 index and a stop then find each other by binary
608/// search, where they used to find each other by walking every character of
609/// the document from the top: a frontend converts an `NSRange` end per
610/// selection change and per misspelled word, and each conversion was a whole
611/// document's work.
612///
613/// Built once per map and never maintained. The stops are private and fixed
614/// for the map's life; the rows only lend the character each stop draws, and
615/// a frontend that reshapes the rows after the build (the terminal inserts
616/// blank filler rows) neither changes those characters nor asks this.
617#[derive(Clone, Default)]
618struct Spelling {
619    /// Parallel to [`VisualMap::stops`].
620    chars: Vec<Option<char>>,
621    /// One longer than `chars`: `utf16[i]` is the UTF-16 length of the text
622    /// before stop `i`, and the last entry the length of the whole text.
623    utf16: Vec<usize>,
624}
625
626impl VisualMap {
627    pub fn num_rows(&self) -> usize {
628        self.rows.len()
629    }
630
631    /// The family name a glyph's [`FaceRef::Named`] stands for, or `None` for
632    /// an id from another map — which a frontend draws in the theme's body
633    /// face, as it draws a family it cannot resolve.
634    pub fn face_name(&self, id: FaceId) -> Option<&str> {
635        self.faces.name(id)
636    }
637
638    /// Every named family this map draws — how a frontend warms a font cache
639    /// before it lays a frame out. See [`faces`](Self::faces).
640    ///
641    /// [`faces`]: VisualMap::faces
642    pub fn faces(&self) -> &FaceTable {
643        &self.faces
644    }
645
646    /// The width of `row` in display columns — the rightmost column its caret
647    /// can occupy, and so what a goal column is clamped to on the way in.
648    pub fn row_width(&self, row: usize) -> usize {
649        self.rows.get(row).map_or(0, |r| r.width())
650    }
651
652    /// The screen `(row, col)` for a source offset — where to draw the caret:
653    /// the *nearest* stop at or past `off`. Snaps a hidden offset (inside a
654    /// delimiter) to the next visible glyph, and never resolves onto decoration
655    /// (a table border, a cell's padding), which is drawn but holds no caret.
656    ///
657    /// "Nearest" rather than "the first one found" because a table's wrapped
658    /// cells put rows slightly out of offset order: scanning top to bottom, the
659    /// second line of column 1 comes *after* the first line of column 2 but
660    /// holds smaller offsets. Where rows are in order the two rules agree.
661    ///
662    /// A soft wrap is the one place two rows want the same offset: the row above
663    /// ends where the row below opens, the space the wrap ate being drawn on the
664    /// row above and the offset past it being the row below's first character.
665    /// It resolves *downstream*, to the row that character is on — the row
666    /// above's last column is a phantom, a place the caret can be drawn but
667    /// never sent, and resolving upstream into it is what pinned Down at the
668    /// first wrap of a paragraph: it aimed at the row below's column 0, landed
669    /// on the offset it already had, and read that back as the row above's end.
670    ///
671    /// The walk is over the rows, not their text: a row before `off` is
672    /// dismissed by looking at where it opens and where it reaches — its first
673    /// stop and its last — rather than at every glyph between, so the cost of
674    /// a lookup mid-document is a few hundred rows' worth of two glyphs each,
675    /// and the one row that holds the answer is the only one read through.
676    /// It used to read every row through, and worse: the row's end candidate
677    /// was built eagerly, and building it measured the row's width, which
678    /// segments the row's whole text into grapheme clusters — so every row
679    /// before `off` paid a full cluster walk to learn it held nothing, and a
680    /// document of long paragraphs paid milliseconds per caret placement.
681    ///
682    /// Not a binary search, though the rows' opening stops ascend. A table's
683    /// wrapped cells put several rows before the one an offset opens on that
684    /// can still hold it, and `end_src` does not ascend across those rows at
685    /// all, so a search would need a prefix table over the rows — and
686    /// [`rows`](Self::rows) is public, reshaped by a frontend after the build
687    /// (the terminal inserts filler rows under a heading), which is exactly
688    /// what a table over it could not survive. The walk needs no table.
689    pub fn pos_of_offset(&self, off: usize) -> (usize, usize) {
690        // (src, row, the glyph — or `None` for the caret past the row's end)
691        let mut best: Option<(usize, usize, Option<usize>)> = None;
692        for (r, row) in self.rows.iter().enumerate() {
693            if row.decoration {
694                continue;
695            }
696            // A row's *first* stop never decreases from one row to the next —
697            // true even across a table's wrapped cells, since a cell's lines run
698            // downward. So once a row opens past the best found so far, no later
699            // row can beat it and the scan stays proportional to `off`.
700            let open = row
701                .glyphs
702                .iter()
703                .find(|g| g.stop)
704                .map_or(row.end_src, |g| g.src);
705            if best.is_some_and(|b| open > b.0) {
706                break;
707            }
708            // Offsets ascend *within* a row, so its last stop or its end is as
709            // far as it reaches: a row that reaches short of `off` has nothing
710            // to offer, and says so from its two ends.
711            let reach = row
712                .glyphs
713                .iter()
714                .rev()
715                .find(|g| g.stop)
716                .map_or(row.end_src, |g| g.src.max(row.end_src));
717            if reach < off {
718                continue;
719            }
720            // And so its first stop at or past `off` is the best it has.
721            let cand = row
722                .glyphs
723                .iter()
724                .enumerate()
725                .find(|(_, g)| g.stop && g.src >= off)
726                .map(|(i, g)| (g.src, r, Some(i)))
727                .or_else(|| (row.end_src >= off).then_some((row.end_src, r, None)));
728            // `<=`, so a tie goes to the later row: the only offset two rows
729            // both hold is a wrap boundary, and it belongs to the row below.
730            if let Some(c) = cand
731                && best.is_none_or(|b| c.0 <= b.0)
732            {
733                best = Some(c);
734            }
735        }
736        match best {
737            Some((_, r, Some(i))) => (r, self.rows[r].col_of_glyph(i)),
738            Some((_, r, None)) => (r, self.rows[r].width()),
739            None => {
740                let r = self.last_stop_row();
741                (r, self.row_width(r))
742            }
743        }
744    }
745
746    /// The rows a source range occupies, inclusive: `(first, last)`.
747    ///
748    /// A *different question* from [`pos_of_offset`](Self::pos_of_offset), which
749    /// is why it can't be spelled with two calls to it. That one answers "where
750    /// does the caret go", and for a caret its forward snap is right — an offset
751    /// inside a hidden delimiter has no column of its own, so the caret belongs
752    /// at the next visible glyph, wherever that turns out to be. This one asks
753    /// "which rows does this block cover", and there the snap is a trap: a
754    /// footnote whose body *ends* in a link (`[^2]: [title](url)`) has a last
755    /// byte inside the hidden destination, so `pos_of_offset(end - 1)` walked
756    /// clean off the note's row and landed on the next note's — and a peek
757    /// slicing `first..=last` out of the frame drew two notes where the reader
758    /// asked for one. Every block ending in a link, an image, or any trailing
759    /// hidden markup had the same fault; only a block ending in visible text
760    /// (which is what the tests happened to use) did not.
761    ///
762    /// `row.end_src` is no help either: it is where the *rendered* text of a row
763    /// ends, not how far into the source the block reaches, and redefining it
764    /// would move every end-of-line caret.
765    ///
766    /// So the last row is found by asking which rows *open* before the range
767    /// does, rather than by mapping its last byte: a row belongs to the range
768    /// when its first caret stop lies before `range.end`. Decoration is skipped
769    /// (a drawn gap between blocks is not part of either), and the answer is
770    /// never shorter than one row — a range whose every byte is hidden still
771    /// covers the row it started on.
772    pub fn row_range_for(&self, range: Range<usize>) -> (usize, usize) {
773        if self.rows.is_empty() {
774            return (0, 0);
775        }
776        let first = self.pos_of_offset(range.start).0;
777        let mut last = first;
778        for (r, row) in self.rows.iter().enumerate().skip(first) {
779            if row.decoration {
780                continue;
781            }
782            let open = row
783                .glyphs
784                .iter()
785                .find(|g| g.stop)
786                .map_or(row.end_src, |g| g.src);
787            if open >= range.end {
788                // A row's first stop never decreases from one row to the next —
789                // the invariant `pos_of_offset` breaks on, true even across a
790                // table's wrapped cells — so nothing below can be in range.
791                break;
792            }
793            last = r;
794        }
795        (first, last)
796    }
797
798    /// The source offset of the task checkbox drawn at `(row, col)`, or `None`
799    /// when that cell holds no box — the hit-test a frontend runs on a click
800    /// before treating it as a tick rather than a caret placement.
801    ///
802    /// Only the box's own cells answer. Clicking an item's *text* places the
803    /// caret like any other click, so the box is a target aimed at rather than
804    /// something tripped over while editing — which is also why this is a
805    /// separate question from [`offset_of_pos`](Self::offset_of_pos) instead of
806    /// a flag on the offset it returns.
807    pub fn task_box_at(&self, row: usize, col: usize) -> Option<usize> {
808        let r = self.rows.get(row)?;
809        self.task_box_at_glyph(row, r.glyph_at_col(col)?)
810    }
811
812    /// [`task_box_at`](Self::task_box_at) keyed by glyph index rather than
813    /// display column — for a frontend that shapes its own rows (the GUI) and so
814    /// resolves a click to a glyph before it ever has a column.
815    pub fn task_box_at_glyph(&self, row: usize, glyph: usize) -> Option<usize> {
816        let r = self.rows.get(row)?;
817        r.task?;
818        let g = r.glyphs.get(glyph)?;
819        (g.style.role == Role::ListMarker).then_some(g.src)
820    }
821
822    /// The source offset for a screen `(row, col)` — where a click or a
823    /// visual-space move lands the caret. Clicking decoration maps through its
824    /// `src`, which points at the text it decorates, so a click on a border or
825    /// on a cell's padding lands in that cell.
826    ///
827    /// The inverse of [`pos_of_offset`](Self::pos_of_offset), which it has to
828    /// agree with: `col` is a display column, and the one it names may be the
829    /// far cell of a wide glyph — [`VRow::glyph_at_col`] is where that lands.
830    pub fn offset_of_pos(&self, row: usize, col: usize) -> usize {
831        let Some(r) = self.rows.get(row) else {
832            // A click or drag below the last row — a short document with empty
833            // space under it, dragged into to extend a selection. Land on the
834            // document's last caret stop (its end), not offset 0: jumping the
835            // caret to the top is the wrong direction, and 0 isn't even a stop
836            // when the document opens on hidden frontmatter or a `# ` marker, so
837            // returning it would leave the caret where it draws in one place and
838            // types in another (`move_to` would then clamp it onto the unhomeable
839            // frontmatter floor). `None` only for a document with no stops at all
840            // (empty), where the caret has nowhere to be but 0.
841            return self.stops.last().copied().unwrap_or(0);
842        };
843        match r.glyph_at_col(col).and_then(|i| r.glyphs.get(i)) {
844            // A glyph that holds no caret is clickable, but where it points
845            // isn't always somewhere the caret can be: the blank gap between two
846            // paragraphs stands at an offset that belongs to neither of them,
847            // and the tail of a grapheme cluster stands inside a character.
848            // Land on the nearest real stop instead of handing back an offset
849            // that looks like the gap but types into the paragraph above.
850            Some(g) if !g.stop => self.nearest_stop(g.src),
851            Some(g) => g.src,
852            // A row's end is a stop by construction — unless the row is
853            // decoration, which contributes none.
854            None if r.decoration => self.nearest_stop(r.end_src),
855            None => r.end_src,
856        }
857    }
858
859    /// Which of a block media's two caret homes `off` is, or `None` for every
860    /// other offset in the document.
861    ///
862    /// [`block_media`](Builder::block_media) gives a block-level image, video, or
863    /// audio exactly two stops — one in front of it and one just past it — and
864    /// nothing inside the markup. Both are ordinary offsets to everything else in
865    /// core, but they are the two places where inserting text would *dissolve the
866    /// picture*: `![](p.png)` with anything typed against it is no longer a block
867    /// image but a paragraph with an inline one, and the frontend that was
868    /// painting a photo there paints a text run instead. A caller that is about to
869    /// insert asks this so it can open a paragraph first — see
870    /// [`Doc::insert`](crate::Doc::insert).
871    ///
872    /// An *inline* image reports `None`: it has no placeholder row and no stops of
873    /// its own, and typing beside one is ordinary editing.
874    ///
875    /// Answers with the media's own source span as well, since a caller that has
876    /// to keep the picture whole usually has to address it — [`Doc::backspace`]
877    /// takes the picture out in one piece rather than nibbling a byte off its
878    /// markup, which is the same dissolution from the other side.
879    ///
880    /// [`Doc::backspace`]: crate::Doc::backspace
881    pub fn block_media_stop(&self, off: usize) -> Option<(MediaStop, Range<usize>)> {
882        for m in &self.media {
883            let Some(row) = self.rows.get(m.rows_span.start) else {
884                continue;
885            };
886            // Every glyph of the `🖼 alt` label maps to the media's start offset;
887            // the row's end is past its markup. Read the start off the label
888            // rather than the first glyph, which on a quoted or listed picture is
889            // the block prefix and points at the gutter.
890            let Some(start) = row
891                .glyphs
892                .iter()
893                .find(|g| g.style.role == Role::Image)
894                .map(|g| g.src)
895            else {
896                continue;
897            };
898            if off == start {
899                return Some((MediaStop::Before, start..row.end_src));
900            }
901            if off == row.end_src {
902                return Some((MediaStop::After, start..row.end_src));
903            }
904        }
905        None
906    }
907
908    /// Whether `off` is a table's trailing caret stop — the one home past a
909    /// table's last cell, at the block's own end ([`TableInfo::end_src`]).
910    ///
911    /// The table's peer of [`block_media_stop`](Self::block_media_stop)'s
912    /// `After`: text inserted at that offset joins the table's last source
913    /// line, and a line glued under a table is a row of it (`| 1 | 2 |x`), so
914    /// a caller about to insert there opens a paragraph first — see
915    /// [`Doc::insert`](crate::Doc::insert). Nothing else about the offset is
916    /// special: it is where Down from the last row lands and where a click in
917    /// the blank space under a trailing table lands.
918    pub fn table_end_stop(&self, off: usize) -> bool {
919        self.tables.iter().any(|t| t.end_src == off)
920    }
921
922    /// Snap `off` to the nearest caret stop — the funnel a frontend that
923    /// hit-tests pixels straight to a source offset must run its result through.
924    /// A click or drag can land in the blank gap a paragraph break is drawn with,
925    /// or inside a hidden delimiter; both are offsets the caret can't rest at, so
926    /// resting there would draw the caret in one place and type in another. This
927    /// settles it on a real caret home instead. Idempotent on an offset that is
928    /// already a stop — the `(row, col)` click path already snaps this way inside
929    /// [`offset_of_pos`](Self::offset_of_pos), and this gives the pixel path the
930    /// same guarantee. Returns `off` unchanged only for an empty document (no
931    /// stops at all).
932    pub fn snap_to_stop(&self, off: usize) -> usize {
933        self.nearest_stop(off)
934    }
935
936    /// The caret stop nearest `off`, preferring the one before it when `off`
937    /// falls exactly between two. Returns `off` unchanged if there are no stops
938    /// at all (an empty document). A mark's content end counts: it is a place
939    /// the caret rests, and the one a drag ending on a marked word means.
940    fn nearest_stop(&self, off: usize) -> usize {
941        let before = Self::last_at_or_before(&self.stops, off)
942            .max(Self::last_at_or_before(&self.mark_ends, off));
943        let after = match (
944            Self::first_at_or_after(&self.stops, off),
945            Self::first_at_or_after(&self.mark_ends, off),
946        ) {
947            (Some(a), Some(b)) => Some(a.min(b)),
948            (a, b) => a.or(b),
949        };
950        match (before, after) {
951            (Some(b), Some(a)) if off - b <= a - off => b,
952            (_, Some(a)) => a,
953            (Some(b), None) => b,
954            (None, None) => off,
955        }
956    }
957
958    /// The glyph stop nearest `off` — [`nearest_stop`](Self::nearest_stop)
959    /// for a walk that pairs stops with characters, which a mark's content
960    /// end has none of. A caret resting on one resolves to the glyph stop
961    /// drawn at the same spot, the one just past the hidden delimiter, so the
962    /// text a system input is shown from there and the steps it counts agree.
963    pub fn snap_to_glyph_stop(&self, off: usize) -> usize {
964        if self.mark_ends.binary_search(&off).is_ok()
965            && let Some(next) = Self::first_at_or_after(&self.stops, off)
966        {
967            return next;
968        }
969        let before = Self::last_at_or_before(&self.stops, off);
970        let after = Self::first_at_or_after(&self.stops, off);
971        match (before, after) {
972            (Some(b), Some(a)) if off - b <= a - off => b,
973            (_, Some(a)) => a,
974            (Some(b), None) => b,
975            (None, None) => off,
976        }
977    }
978
979    /// The last of `sorted` at or before `off`, if any.
980    fn last_at_or_before(sorted: &[usize], off: usize) -> Option<usize> {
981        let i = sorted.partition_point(|&s| s <= off);
982        i.checked_sub(1).map(|i| sorted[i])
983    }
984
985    /// The first of `sorted` at or after `off`, if any.
986    fn first_at_or_after(sorted: &[usize], off: usize) -> Option<usize> {
987        let i = sorted.partition_point(|&s| s < off);
988        sorted.get(i).copied()
989    }
990
991    /// The next place the caret rests past `off` — the next glyph stop or the
992    /// next mark's content end, whichever comes first. What Right walks:
993    /// leaving `**bold**` from the `d` is two presses, one onto the end of the
994    /// bold (still bold, the toolbar lit) and one past its delimiter, at the
995    /// same spot on screen. [`stop_after`](Self::stop_after) is the walk that
996    /// skips the first, for every caller that pairs stops with characters.
997    pub fn caret_stop_after(&self, off: usize) -> Option<usize> {
998        match (
999            self.stop_after(off),
1000            Self::first_at_or_after(&self.mark_ends, off + 1),
1001        ) {
1002            (Some(a), Some(b)) => Some(a.min(b)),
1003            (a, b) => a.or(b),
1004        }
1005    }
1006
1007    /// The previous place the caret rests before `off` — the mirror of
1008    /// [`caret_stop_after`](Self::caret_stop_after), what Left walks.
1009    pub fn caret_stop_before(&self, off: usize) -> Option<usize> {
1010        self.stop_before(off).max(
1011            off.checked_sub(1)
1012                .and_then(|o| Self::last_at_or_before(&self.mark_ends, o)),
1013        )
1014    }
1015
1016    /// Whether the caret can occupy `row` at all: decoration rows (a table's
1017    /// border rules) are stepped over by vertical motion.
1018    pub fn row_is_navigable(&self, row: usize) -> bool {
1019        self.rows.get(row).is_some_and(|r| !r.decoration)
1020    }
1021
1022    /// The first offset the caret can rest at on `row` — its first stop, or the
1023    /// row's own end when it holds no text (an empty paragraph). `None` for a
1024    /// decoration row, which holds no caret at all.
1025    ///
1026    /// Not `offset_of_pos(row, 0)`: column 0 of a quoted or listed row is the
1027    /// gutter, and a gutter's `src` points at the *block* it opens, so the stop
1028    /// nearest it is the one on the block's first row rather than on this one.
1029    /// Which is right for a click — the gutter decorates the whole block — and
1030    /// wrong for Home, whose whole question is where *this* row starts.
1031    pub fn row_start(&self, row: usize) -> Option<usize> {
1032        let r = self.rows.get(row).filter(|r| !r.decoration)?;
1033        Some(
1034            r.glyphs
1035                .iter()
1036                .find(|g| g.stop)
1037                .map_or(r.end_src, |g| g.src),
1038        )
1039    }
1040
1041    /// The last row the caret can rest on — the fallback when an offset is past
1042    /// everything rendered (a table's bottom border must not swallow the caret).
1043    fn last_stop_row(&self) -> usize {
1044        (0..self.rows.len())
1045            .rev()
1046            .find(|&r| self.row_is_navigable(r))
1047            .unwrap_or(0)
1048    }
1049
1050    /// The nearest row above `row` the caret can occupy, skipping decoration.
1051    pub fn navigable_above(&self, row: usize) -> Option<usize> {
1052        (0..row.min(self.rows.len()))
1053            .rev()
1054            .find(|&r| self.row_is_navigable(r))
1055    }
1056
1057    /// The nearest row below `row` the caret can occupy, skipping decoration.
1058    pub fn navigable_below(&self, row: usize) -> Option<usize> {
1059        ((row + 1)..self.rows.len()).find(|&r| self.row_is_navigable(r))
1060    }
1061
1062    /// The caret stop just before `off` — one press of Left. `None` at the
1063    /// first stop in the document.
1064    ///
1065    /// Runs of decoration (a table border, a cell's alignment padding) are
1066    /// stepped over in a single press: they hold no stop, so they aren't in the
1067    /// table to land on.
1068    pub fn stop_before(&self, off: usize) -> Option<usize> {
1069        let i = self.stops.partition_point(|&s| s < off);
1070        i.checked_sub(1).map(|i| self.stops[i])
1071    }
1072
1073    /// The caret stop just after `off` — one press of Right. `None` at the last
1074    /// stop in the document.
1075    pub fn stop_after(&self, off: usize) -> Option<usize> {
1076        let i = self.stops.partition_point(|&s| s <= off);
1077        self.stops.get(i).copied()
1078    }
1079
1080    /// The first caret stop at or past `off` — where the caret at a hidden
1081    /// offset is *drawn*, and so where a rightward walk over the rendered text
1082    /// starts from.
1083    pub fn stop_at_or_after(&self, off: usize) -> Option<usize> {
1084        let i = self.stops.partition_point(|&s| s < off);
1085        self.stops.get(i).copied()
1086    }
1087
1088    /// The last caret stop at or before `off` — where a leftward walk starts
1089    /// from. Snapping the way the walk is headed, rather than always forward,
1090    /// is what keeps a leftward motion from ever moving the caret right.
1091    pub fn stop_at_or_before(&self, off: usize) -> Option<usize> {
1092        let i = self.stops.partition_point(|&s| s <= off);
1093        i.checked_sub(1).map(|i| self.stops[i])
1094    }
1095
1096    /// Whether the caret may rest at `off` — the invariant every motion in this
1097    /// view has to leave standing. A glyph stop, a row's end, or a hidden
1098    /// mark's content end ([`mark_ends`](Self::mark_ends)).
1099    pub fn is_stop(&self, off: usize) -> bool {
1100        self.stops.binary_search(&off).is_ok() || self.mark_ends.binary_search(&off).is_ok()
1101    }
1102
1103    /// The visible text a caret crosses walking rightward from `from` up to
1104    /// (but not including) `to` — `UITextInput.text(in:)`'s `[from, to)` in
1105    /// *this* view. A hidden inline-mark delimiter (`**`, `` ` ``, `_`, an
1106    /// escape backslash) never got a glyph in the first place — see
1107    /// [`push_text`]/[`synth`] — so it contributes nothing; what's left is
1108    /// what's drawn on screen for that span, one character per caret stop.
1109    ///
1110    /// **Exactly one character per stop** is the contract, and it is the
1111    /// system text input's, not a nicety: `UITextInput`'s tokenizer reads a
1112    /// window of this text around a tap, indexes into it by the integer
1113    /// `offset(from:to:)` reports (`distance_offset` in `leaf-ffi`, a count of
1114    /// [`stop_after`](Self::stop_after) hops), finds a word boundary at some
1115    /// character index, and hands the delta back through
1116    /// `position(from:offset:)`, which hops stops again. If the text ever
1117    /// spends a character on something that is not a stop, or a stop on
1118    /// nothing, every index past that point is off by one and the word the
1119    /// reader double-tapped comes back shifted — into the header row of a
1120    /// table, or one letter short. So a stop that draws a glyph is spelled
1121    /// as that glyph, and a stop that draws none is spelled `'\n'`:
1122    ///
1123    /// - a row's own end stop ([`VRow::end_src`]) — the caret home past a
1124    ///   paragraph's, heading's, list item's, or code line's last glyph. This
1125    ///   is also what keeps two blocks' words apart: without it the last word
1126    ///   of one paragraph and the first of the next read as one run of
1127    ///   letters (`"…edb\n\nhello\n"` came back as `"edbhello"`), and the
1128    ///   tokenizer selected across the boundary. A list item's end is a
1129    ///   row end like any other, though no blank gap row follows it.
1130    /// - a table cell's end, which [`push_table_row`] draws as the gutter
1131    ///   space before the next `│` so the caret has somewhere to stand past
1132    ///   the cell's last character. To a reader of *this* text a cell ends a
1133    ///   line: spelled as a space, a touch surface that lands a tap at a
1134    ///   word's end past the space that follows it stepped into the next
1135    ///   cell — or the next row, from the last column.
1136    ///
1137    /// A hidden mark's content end ([`mark_ends`](Self::mark_ends)) is a place
1138    /// the caret rests but not a stop the walks above count, so it has no
1139    /// character here either; `from` is snapped to the glyph stop drawn at
1140    /// the same spot first, exactly as [`snap_to_glyph_stop`] does for those
1141    /// walks. `to` is left as given, so a stop landing exactly on it is still
1142    /// excluded — the same half-open range `distance_offset`'s loop counts.
1143    ///
1144    /// [`push_table_row`]: Builder::push_table_row
1145    /// [`snap_to_glyph_stop`]: Self::snap_to_glyph_stop
1146    pub fn visible_text(&self, from: usize, to: usize) -> String {
1147        self.visible_items(from, to)
1148            .into_iter()
1149            .map(|(_, ch)| ch.unwrap_or('\n'))
1150            .collect()
1151    }
1152
1153    /// The UTF-16 length of `visible_text(from, to)` — what an `NSRange`
1154    /// location into that text is, without building the string.
1155    ///
1156    /// AppKit's `NSTextInputClient` and `NSAccessibility` speak in UTF-16
1157    /// units of *the text as the system sees it*, which for leaf is the visible
1158    /// text — delimiters hidden. A frontend reporting its selection to the
1159    /// system converts each end with this and gets back an index into the
1160    /// string `visible_text(0, end)` returns, which is exactly what the system
1161    /// will index into.
1162    pub fn visible_utf16_len(&self, from: usize, to: usize) -> usize {
1163        let (lo, hi) = self.visible_span(from, to);
1164        let utf16 = &self.spelling().utf16;
1165        utf16[hi] - utf16[lo]
1166    }
1167
1168    /// The inverse of `visible_utf16_len(0, ·)`: the source offset of the
1169    /// visible character a UTF-16 index into `visible_text(0, to)` lands on.
1170    ///
1171    /// An index inside a surrogate pair resolves to the character that owns
1172    /// it; one at or past the end of the text returns `None`, so a caller can
1173    /// substitute the document's end stop. The `\n` a row's or a cell's end
1174    /// is spelled with resolves to that end stop — a caret home, so a caller
1175    /// placing a caret there needs no snap.
1176    pub fn offset_at_visible_utf16(&self, to: usize, index: usize) -> Option<usize> {
1177        let (lo, hi) = self.visible_span(0, to);
1178        let utf16 = &self.spelling().utf16;
1179        // The stop whose text ends past the index is the one it lands on; a
1180        // stop whose text ends at or before it lies wholly before it.
1181        let target = utf16[lo] + index;
1182        let i = lo + utf16[lo + 1..=hi].partition_point(|&end| end <= target);
1183        (i < hi).then(|| self.stops[i])
1184    }
1185
1186    /// The items `visible_text` spells, in order — one per caret stop in
1187    /// `[from, to)`, keyed by the stop's source offset: the glyph it draws
1188    /// (`Some`), or `None` for a stop with no character of its own, which the
1189    /// text spells `'\n'`. See [`visible_text`](Self::visible_text) for which
1190    /// stops those are and why.
1191    fn visible_items(&self, from: usize, to: usize) -> Vec<(usize, Option<char>)> {
1192        let (lo, hi) = self.visible_span(from, to);
1193        self.stops[lo..hi]
1194            .iter()
1195            .copied()
1196            .zip(self.spelling().chars[lo..hi].iter().copied())
1197            .collect()
1198    }
1199
1200    /// The stops `visible_text(from, to)` spells, as a range into the stop
1201    /// table: from the glyph stop `from` snaps to, up to but excluding the
1202    /// first stop at or past `to`.
1203    fn visible_span(&self, from: usize, to: usize) -> (usize, usize) {
1204        let from = self.snap_to_glyph_stop(from);
1205        let lo = self.stops.partition_point(|&s| s < from);
1206        // The document's last stop is the end of the text, not a character in
1207        // it: `distance_offset` has no hop past it to pair one with.
1208        let last = self.stops.len().saturating_sub(1);
1209        let hi = self.stops.partition_point(|&s| s < to).min(last).max(lo);
1210        (lo, hi)
1211    }
1212
1213    /// The [`Spelling`] of this map's stops, tabulated on first use.
1214    fn spelling(&self) -> &Spelling {
1215        self.spelling.get_or_init(|| {
1216            // The glyph each stop draws — the first at its offset in row
1217            // order, since a media row's label glyphs all share the media's
1218            // offset and a wrapped line's end is the next line's first glyph.
1219            // Row order only follows source order outside a table's wrapped
1220            // cells (see `pos_of_offset`), which is why each glyph looks its
1221            // stop up rather than the two being walked side by side.
1222            //
1223            // Within a row offsets ascend, so a cursor into the stop table
1224            // walks forward with the row's glyphs and the whole pass is a
1225            // read of the glyphs plus one search per row — a keystroke's
1226            // first conversion pays for this, so it is kept to that. The
1227            // search is repeated only if a row's offsets step back, which
1228            // none does; the walk is correct either way.
1229            let stops = &self.stops;
1230            let mut chars: Vec<Option<char>> = vec![None; stops.len()];
1231            for row in self.rows.iter().filter(|r| !r.decoration) {
1232                let mut i = 0;
1233                let mut prev = usize::MAX;
1234                for g in row.glyphs.iter().filter(|g| g.stop) {
1235                    if prev == usize::MAX || g.src < prev {
1236                        i = stops.partition_point(|&s| s < g.src);
1237                    } else {
1238                        while i < stops.len() && stops[i] < g.src {
1239                            i += 1;
1240                        }
1241                    }
1242                    prev = g.src;
1243                    if i < stops.len() && stops[i] == g.src && chars[i].is_none() {
1244                        chars[i] = Some(g.ch);
1245                    }
1246                }
1247            }
1248            // A cell's end stop has a glyph (the gutter space) but is spelled
1249            // as a line end; the structural grid is where the cells' offsets
1250            // live.
1251            for cell in self
1252                .tables
1253                .iter()
1254                .flat_map(|t| t.grid.iter())
1255                .flat_map(|r| r.cells.iter())
1256            {
1257                if let Ok(i) = self.stops.binary_search(&cell.end) {
1258                    chars[i] = None;
1259                }
1260            }
1261            let mut utf16 = Vec::with_capacity(chars.len() + 1);
1262            utf16.push(0);
1263            for ch in &chars {
1264                let before = *utf16.last().unwrap_or(&0);
1265                utf16.push(before + ch.map_or(1, char::len_utf16));
1266            }
1267            Spelling { chars, utf16 }
1268        })
1269    }
1270}
1271
1272/// Collect the caret stops of a laid-out grid: every stop glyph's offset plus
1273/// every row's end, ascending and deduplicated. Duplicates are the norm rather
1274/// than the exception — a wrapped line's end is the same offset as the next
1275/// line's first glyph — and collapsing them is what makes one press of Left or
1276/// Right cross exactly one stop.
1277fn collect_stops(rows: &[VRow]) -> Vec<usize> {
1278    let mut stops: Vec<usize> = rows
1279        .iter()
1280        .filter(|r| !r.decoration)
1281        .flat_map(|r| {
1282            r.glyphs
1283                .iter()
1284                .filter(|g| g.stop)
1285                .map(|g| g.src)
1286                .chain(std::iter::once(r.end_src))
1287        })
1288        .collect();
1289    stops.sort_unstable();
1290    stops.dedup();
1291    stops
1292}
1293
1294/// Collect every row's [`VRow::mark_ends`] into one ascending, deduplicated
1295/// table — the peer of [`collect_stops`] for the caret's second home at the
1296/// end of a hidden mark. A mark that closes at a row's end coincides with the
1297/// row's own end stop; that offset is in both tables, and harmlessly so.
1298fn collect_mark_ends(rows: &[VRow]) -> Vec<usize> {
1299    let mut ends: Vec<usize> = rows
1300        .iter()
1301        .filter(|r| !r.decoration)
1302        .flat_map(|r| r.mark_ends.iter().copied())
1303        .collect();
1304    ends.sort_unstable();
1305    ends.dedup();
1306    ends
1307}
1308
1309/// Group the rows tagged [`VRow::code`] into one [`CodeBlockInfo`] per maximal
1310/// run — the block-level view a frontend needs to box and scroll each code
1311/// block. Two code blocks are always parted by the blank separator row a block
1312/// boundary is spelled with (never itself a code row), so a contiguous run is
1313/// exactly one block. Derived from the final rows rather than tracked through
1314/// the builder so it comes out right no matter how [`build_cached`] and
1315/// [`build_spliced`] shuffle rows around.
1316fn code_block_spans(rows: &[VRow]) -> Vec<CodeBlockInfo> {
1317    let mut blocks = Vec::new();
1318    let mut start: Option<usize> = None;
1319    for (i, row) in rows.iter().enumerate() {
1320        match (row.code, start) {
1321            (true, None) => start = Some(i),
1322            (false, Some(s)) => {
1323                blocks.push(CodeBlockInfo {
1324                    rows_span: s..i,
1325                    lang: rows[s].code_lang.clone(),
1326                });
1327                start = None;
1328            }
1329            _ => {}
1330        }
1331    }
1332    if let Some(s) = start {
1333        blocks.push(CodeBlockInfo {
1334            rows_span: s..rows.len(),
1335            lang: rows[s].code_lang.clone(),
1336        });
1337    }
1338    blocks
1339}
1340
1341/// The value of `node`'s `key` attribute, if it carries one *with* a value. A
1342/// bare attribute (`controls`, `muted`) has a `None` value and so reads as
1343/// absent here — a caller wanting presence-not-value tests the list directly.
1344/// Shared by the media element and `<source>` readers.
1345fn attr_of(node: &FlatNode, key: &str) -> Option<String> {
1346    node.attrs
1347        .iter()
1348        .find(|(k, _)| k == key)
1349        .and_then(|(_, v)| v.clone())
1350}
1351
1352/// Collect one [`MediaInfo`] per row carrying a [`VRow::media`] mark — the
1353/// block-level view a frontend needs to replace each placeholder row with a real
1354/// picture. The mark rides the block's *first* row and names how many rows the
1355/// media reserves ([`MediaMark::rows`]); the rows below it are blank
1356/// [`decoration`](VRow::decoration) fillers that hold the vertical space and no
1357/// caret. So the span runs from the marked row across those fillers. Derived from
1358/// the final rows rather than tracked through the builder so it survives however
1359/// [`build_cached`] and [`build_spliced`] shuffle rows around.
1360fn media_spans(rows: &[VRow]) -> Vec<MediaInfo> {
1361    rows.iter()
1362        .enumerate()
1363        .filter_map(|(i, row)| {
1364            row.media.as_ref().map(|m| MediaInfo {
1365                rows_span: i..i + m.rows.max(1),
1366                kind: m.kind,
1367                destination: m.destination.clone(),
1368                sources: m.sources.clone(),
1369                alt: m.alt.clone(),
1370                poster: m.poster.clone(),
1371            })
1372        })
1373        .collect()
1374}
1375
1376/// Re-label the drawn block boundaries either side of a block-level media
1377/// placeholder, so the pair a frontend spaces by names the picture.
1378///
1379/// [`BlockClass::from_node_kind`] classifies the node the walk is standing on,
1380/// and a block image is never a node of its own: [`Builder::media_only`] promotes
1381/// its *wrapper* — a `paragraph`, or the `container` a `<video>`/`<audio>`
1382/// arrives as — so the gap above a picture reported [`BlockClass::Paragraph`] and
1383/// the gap above a movie reported [`BlockClass::Directive`], the class a frontend
1384/// paints a tinted panel for. [`BlockClass::Media`] was unreachable in
1385/// consequence: the vocabulary named a kind no frontend could ever be told about.
1386///
1387/// Done as a pass over the finished rows rather than inside the walk because
1388/// only the rows know. The incremental top-level walk carries no node arena at
1389/// all (`nodes: &[]`) and can classify by kind alone, so teaching the wrapper
1390/// promotion to the whole-arena walk would label the full and incremental builds
1391/// differently — the exact drift that walk's own comment forbids. Both builds
1392/// emit the same [`VRow::media`] marks, so both reach the same answer here. The
1393/// [`media_spans`] / [`code_block_spans`] pattern.
1394///
1395/// One gap can be spelled with several rows — [`Builder::emit_separators_before`]
1396/// draws the row that closes the block above and the row that opens the block
1397/// below, with any extra blank source lines navigable between them — and gives
1398/// every one of them the same [`Boundary`]. So the walk crosses those navigable
1399/// blanks and relabels the whole run, stopping at the first row that is neither.
1400fn label_media_boundaries(rows: &mut [VRow]) {
1401    // A display formula's placeholder is promoted from its paragraph exactly
1402    // as a picture is, and its gaps are relabelled the same way, as `Math`.
1403    let spans: Vec<(Range<usize>, BlockClass)> = rows
1404        .iter()
1405        .enumerate()
1406        .filter_map(|(i, row)| {
1407            if let Some(m) = &row.media {
1408                Some((i..i + m.rows.max(1), BlockClass::Media))
1409            } else {
1410                row.math
1411                    .iter()
1412                    .find(|m| m.glyph.is_none())
1413                    .map(|m| (i..i + m.rows.max(1), BlockClass::Math))
1414            }
1415        })
1416        .collect();
1417    // A row inside one gap: a drawn boundary to relabel, or one of the navigable
1418    // blank lines sitting between two drawn ones. Anything else ends the run.
1419    fn in_gap(row: &VRow) -> bool {
1420        row.boundary.is_some() || (!row.decoration && row.glyphs.is_empty())
1421    }
1422    for (span, class) in spans {
1423        for i in (0..span.start).rev() {
1424            if !in_gap(&rows[i]) {
1425                break;
1426            }
1427            if let Some(b) = rows[i].boundary.as_mut() {
1428                b.below = class;
1429            }
1430        }
1431        for row in rows.iter_mut().skip(span.end) {
1432            if !in_gap(row) {
1433                break;
1434            }
1435            if let Some(b) = row.boundary.as_mut() {
1436                b.above = class;
1437            }
1438        }
1439    }
1440}
1441
1442/// Collect one [`DirectiveInfo`] per row carrying a [`VRow::leaf_directive`]
1443/// mark — the block-level view a frontend needs to replace each placeholder row
1444/// with whatever the directive means to it. The peer of [`media_spans`], derived
1445/// from the final rows for the same reason: it survives however [`build_cached`]
1446/// and [`build_spliced`] shuffle rows around.
1447fn directive_spans(rows: &[VRow]) -> Vec<DirectiveInfo> {
1448    rows.iter()
1449        .enumerate()
1450        .filter_map(|(i, row)| {
1451            row.leaf_directive.as_ref().map(|m| DirectiveInfo {
1452                rows_span: i..i + m.rows.max(1),
1453                name: m.name.clone(),
1454                attrs: m.attrs.clone(),
1455                label: m.label.clone(),
1456            })
1457        })
1458        .collect()
1459}
1460
1461/// Collect one [`MathInfo`] per [`VRow::math`] mark — the peer of
1462/// [`media_spans`] and [`directive_spans`], derived from the final rows for the
1463/// same reason. A block mark spans its fillers; an atom spans its own row.
1464fn math_spans(rows: &[VRow]) -> Vec<MathInfo> {
1465    rows.iter()
1466        .enumerate()
1467        .flat_map(|(i, row)| {
1468            row.math.iter().map(move |m| {
1469                let src = match m.glyph {
1470                    Some(g) => row.glyphs.get(g).map_or(row.end_src, |g| g.src),
1471                    None => row
1472                        .glyphs
1473                        .iter()
1474                        .find(|g| g.style.role == Role::Math)
1475                        .map_or(row.end_src, |g| g.src),
1476                };
1477                MathInfo {
1478                    rows_span: i..i + m.rows.max(1),
1479                    row: i,
1480                    glyph: m.glyph,
1481                    tex: m.tex.clone(),
1482                    display: m.display,
1483                    src,
1484                }
1485            })
1486        })
1487        .collect()
1488}
1489
1490/// The source range of a fenced code block's info string — everything on the
1491/// opening line past the fence (`` ```rust `` → the `rust`). `block_start` is the
1492/// code block node's `span.start`. `None` for an indented code block, which
1493/// opens with no fence to carry one. The range is empty for a fence written
1494/// bare (`` ``` `` alone), which is exactly where a language would be inserted.
1495///
1496/// A block inside a quote or a list item starts at its line's start, with the
1497/// container's marker in front of the fence — `> ```rust`, `- ```rust` — so
1498/// the marker is skipped first, and the fence's three-space indent allowance
1499/// is measured from where the container's content begins: past the marker on
1500/// the fence's own line, or, on a continuation line, from the content column
1501/// of the list item above. That is what keeps an *indented* code block inside
1502/// a list item answering `None`.
1503///
1504/// Shared by the WYSIWYG builder (to label the box) and [`crate::Doc`] (to edit
1505/// the label through a prompt), so the two agree on where the language lives.
1506pub fn code_info_span(source: &str, block_start: usize) -> Option<Range<usize>> {
1507    let rest = source.get(block_start..)?;
1508    let line_len = rest.find('\n').unwrap_or(rest.len());
1509    let line = &rest[..line_len];
1510    let (depth, quoted) = strip_quote_markers(line);
1511    let mut content = quoted;
1512    let mut listed = false;
1513    while let Some(n) = list_marker_len(&line[content..], 3) {
1514        content += n;
1515        listed = true;
1516    }
1517    let lead = leading_spaces(&line[content..]);
1518    // No marker of its own: a continuation line, whose leading spaces include
1519    // the enclosing item's indent. Only when the block owns its line — a span
1520    // that starts mid-line has had its container prefix measured off already.
1521    let at_line_start = block_start == 0 || source.as_bytes()[block_start - 1] == b'\n';
1522    let base = if listed || !at_line_start {
1523        0
1524    } else {
1525        item_content_column(&source[..block_start], depth, lead)
1526    };
1527    // A fence may be indented up to three spaces; past that it opens with a run
1528    // of the same fence character.
1529    if lead - base > 3 {
1530        return None;
1531    }
1532    let at = content + lead;
1533    let fence = line[at..].chars().next()?;
1534    if fence != '`' && fence != '~' {
1535        return None; // an indented block, not a fenced one
1536    }
1537    let fence_len = line[at..].chars().take_while(|&c| c == fence).count();
1538    let info_start = block_start + at + fence_len;
1539    Some(info_start..block_start + line_len)
1540}
1541
1542/// The spaces a line opens with.
1543fn leading_spaces(s: &str) -> usize {
1544    s.bytes().take_while(|&b| b == b' ').count()
1545}
1546
1547/// A line's block-quote markers — each `>` behind up to three spaces, with the
1548/// one space after it — as the quote depth and the byte offset past them.
1549fn strip_quote_markers(line: &str) -> (usize, usize) {
1550    let b = line.as_bytes();
1551    let (mut depth, mut i) = (0, 0);
1552    loop {
1553        let j = i + leading_spaces(&line[i..]);
1554        if j - i > 3 || b.get(j) != Some(&b'>') {
1555            return (depth, i);
1556        }
1557        depth += 1;
1558        i = j + 1 + usize::from(b.get(j + 1) == Some(&b' '));
1559    }
1560}
1561
1562/// The length of a list marker opening `s` — its indent (at most `max_lead`
1563/// spaces), a bullet or an ordinal, and the one space the item's content
1564/// starts after — or `None` when `s` opens with none.
1565fn list_marker_len(s: &str, max_lead: usize) -> Option<usize> {
1566    let b = s.as_bytes();
1567    let lead = leading_spaces(s);
1568    if lead > max_lead {
1569        return None;
1570    }
1571    let mark = match b.get(lead)? {
1572        b'-' | b'*' | b'+' => 1,
1573        c if c.is_ascii_digit() => {
1574            let digits = b[lead..].iter().take_while(|c| c.is_ascii_digit()).count();
1575            if digits > 9 || !matches!(b.get(lead + digits), Some(b'.' | b')')) {
1576                return None;
1577            }
1578            digits + 1
1579        }
1580        _ => return None,
1581    };
1582    match b.get(lead + mark) {
1583        Some(b' ') => Some(lead + mark + 1),
1584        None => Some(lead + mark),
1585        _ => None,
1586    }
1587}
1588
1589/// The content column of the list item a continuation line indented `lead`
1590/// spaces (past `depth` quote markers) sits in: the nearest item above whose
1591/// content starts at or before `lead`. `0` when a line at the margin, or a
1592/// change of quote depth, says there is no such item.
1593fn item_content_column(before: &str, depth: usize, lead: usize) -> usize {
1594    for line in before.strip_suffix('\n').unwrap_or(before).rsplit('\n') {
1595        let (d, q) = strip_quote_markers(line);
1596        let s = &line[q..];
1597        if s.trim().is_empty() {
1598            continue;
1599        }
1600        if d != depth {
1601            return 0;
1602        }
1603        let mut col = 0;
1604        while let Some(n) = list_marker_len(&s[col..], usize::MAX) {
1605            col += n;
1606        }
1607        if col > 0 && col <= lead {
1608            return col;
1609        }
1610        if col == 0 && leading_spaces(s) == 0 {
1611            return 0;
1612        }
1613    }
1614    0
1615}
1616
1617/// A fenced code block's language for display: its info string, trimmed, or
1618/// `None` when there's no fence or the fence carries no language. The trimmed
1619/// text is what a frontend labels the box with; [`code_info_span`] is what an
1620/// edit replaces.
1621pub fn code_language(source: &str, block_start: usize) -> Option<String> {
1622    let span = code_info_span(source, block_start)?;
1623    let text = source.get(span)?.trim();
1624    (!text.is_empty()).then(|| text.to_string())
1625}
1626
1627/// A horizontal rule's dash count when the map isn't wrapping to a column grid
1628/// (the GUI, which wraps at pixel width): a fixed, sane width the frontend can
1629/// paint or re-wrap, instead of a runaway count from an unbounded wrap width.
1630const UNWRAPPED_RULE_WIDTH: usize = 40;
1631
1632/// The one glyph an inline formula renders to on a surface that paints
1633/// pictures in a line — what a plain surface handed such a map would show.
1634/// One column wide, so a column-wrapped build's arithmetic still holds; the
1635/// frontend that asked for atoms draws the picture as wide as it is.
1636const MATH_ATOM: char = '∑';
1637
1638/// What the surface a map is built for can paint, and how tall its pictures
1639/// came out — the things about a frontend that change the rows core lays
1640/// down, gathered from the frontend by [`crate::Doc`] and threaded into every
1641/// build. The defaults are the plain surface: nothing painted in a line, every
1642/// picture a one-row placeholder, which is what every test that passes
1643/// `Surface::default()` gets and what every build got before there was one.
1644#[derive(Clone, Debug, Default, PartialEq, Eq)]
1645pub struct Surface {
1646    /// How many visual rows each block image reserves, keyed by its
1647    /// destination — the frontend's per-image height, set through
1648    /// [`crate::Doc::set_media_rows`] so [`Builder::block_media`] can size the
1649    /// placeholder without core doing any I/O. A destination absent from the
1650    /// map (or a `0`/`1` entry) reserves the bare one-row placeholder.
1651    pub media_rows: HashMap<String, usize>,
1652    /// The same for each display formula, keyed by its TeX verbatim (the
1653    /// [`MathMark::tex`] a frontend was handed) — set through
1654    /// [`crate::Doc::set_math_rows`] once the frontend has typeset and
1655    /// measured the picture.
1656    pub math_rows: HashMap<String, usize>,
1657    /// Whether the surface can paint a picture *inside* a line of text, so an
1658    /// inline formula may render to one atom glyph it draws over
1659    /// ([`MathInfo`]). A pixel-laid-out frontend says yes; a terminal cannot
1660    /// composite an image over one cell, and leaves this off to keep an inline
1661    /// formula as the code-styled TeX it always showed. Off by default.
1662    pub inline_pictures: bool,
1663}
1664
1665/// The one source line rendering its markup raw — the caret's, when the mode
1666/// or the content asks for it — and how much of the markup that means. See
1667/// [`crate::Doc::reveal_line`], which decides both.
1668///
1669/// Two grades because two things ask. Under
1670/// [`MarkupMode::Full`](crate::MarkupMode::Full) *every* delimiter on the line
1671/// comes back (`markup: true`). In the two hidden modes a formula still
1672/// reveals — its content is its TeX and not its picture, so hiding is not
1673/// enough — and the line is threaded through with `markup: false`: a math node
1674/// meeting it shows its source, and an emphasis meeting it hides its
1675/// asterisks as it always did.
1676#[derive(Clone, Debug, PartialEq, Eq)]
1677pub struct Reveal {
1678    /// The source byte range of the line, newline excluded — empty but present
1679    /// on a blank line.
1680    pub line: Range<usize>,
1681    /// Whether ordinary inline markup reveals on it too, or only math.
1682    pub markup: bool,
1683}
1684
1685impl Reveal {
1686    /// The caret's line under `MarkupMode::Full`: everything on it reveals.
1687    pub fn full(line: Range<usize>) -> Self {
1688        Reveal { line, markup: true }
1689    }
1690
1691    /// The caret's line in a hidden mode, where only a formula reveals.
1692    pub fn math(line: Range<usize>) -> Self {
1693        Reveal {
1694            line,
1695            markup: false,
1696        }
1697    }
1698
1699    /// Whether `span` meets this line — the test every arm that reveals runs.
1700    ///
1701    /// Touching at an endpoint counts: an emphasis ending exactly where the
1702    /// line does is on that line, and a zero-length reveal range (the caret
1703    /// alone on a blank line) still meets a node that starts there. The test
1704    /// is deliberately generous — the failure it avoids is revealing one
1705    /// delimiter of a pair while hiding the other, which looks like corruption
1706    /// rather than like markup.
1707    fn meets(&self, span: &Range<usize>) -> bool {
1708        span.start <= self.line.end && self.line.start <= span.end
1709    }
1710}
1711
1712/// Render the document to a [`VisualMap`]. `wrap` is the column budget for
1713/// word-wrapping (`Some` for the monospace TUI), or `None` to emit one row per
1714/// block — the GUI does its own proportional pixel wrapping over these rows.
1715/// Text and offsets come from the AST (`str` nodes carry the verbatim source
1716/// slice and an exact span), so the original source string isn't needed here.
1717pub fn build(
1718    nodes: &[FlatNode],
1719    source: &str,
1720    wrap: Option<usize>,
1721    preserve_soft: bool,
1722    surface: &Surface,
1723    reveal: Option<Reveal>,
1724) -> VisualMap {
1725    let Some(doc) = nodes.iter().position(|n| n.kind == Kind::Doc) else {
1726        return VisualMap::default();
1727    };
1728    let top = top_level(nodes, doc);
1729    let mut b = Builder {
1730        nodes,
1731        source,
1732        wrap: wrap.map(|w| w.max(8)),
1733        rows: Vec::new(),
1734        tables: Vec::new(),
1735        last_off: 0,
1736        stepped_over: 0,
1737        surface,
1738        break_glyph: Cell::new(' '),
1739        preserve_soft,
1740        reveal: reveal.clone(),
1741        pending_mark_ends: RefCell::new(Vec::new()),
1742        pending_math: RefCell::new(Vec::new()),
1743        saw_math: Cell::new(false),
1744        presentation: Presentation::default(),
1745        faces: RefCell::new(FaceTable::default()),
1746    };
1747    // The hidden frontmatter's end is the baseline for the leading and trailing
1748    // blank rows and for the caret floor — see [`hidden_prefix_end`].
1749    // `top_level` has already dropped every `metadata` child, so read it off
1750    // the arena.
1751    let hidden_end = hidden_prefix_end(source, metadata_end_of(nodes, doc));
1752    let last_drawn = b.top_blocks(&top, hidden_end);
1753    b.emit_trailing_blank_lines(last_drawn.unwrap_or(BlockClass::Paragraph), hidden_end);
1754    let content_start = floor_of(
1755        &b.rows,
1756        top.first().map_or(hidden_end, |&i| nodes[i].span.start),
1757    );
1758    let stops = collect_stops(&b.rows);
1759    let mark_ends = collect_mark_ends(&b.rows);
1760    label_media_boundaries(&mut b.rows);
1761    let code_blocks = code_block_spans(&b.rows);
1762    let media = media_spans(&b.rows);
1763    let directives = directive_spans(&b.rows);
1764    let math = math_spans(&b.rows);
1765    VisualMap {
1766        rows: b.rows,
1767        content_start,
1768        stops,
1769        mark_ends,
1770        tables: b.tables,
1771        code_blocks,
1772        media,
1773        directives,
1774        math,
1775        faces: b.faces.into_inner(),
1776        spelling: OnceLock::new(),
1777    }
1778}
1779
1780/// Like [`build`], but reuses a persistent [`BlockCache`] so an edit re-renders
1781/// only the top-level blocks whose source bytes changed *and* marshals only
1782/// those blocks from twig instead of the whole arena.
1783///
1784/// `top` is the document's top-level blocks — twig's `child_spans` of the doc
1785/// root: `(node_id, kind, span)` for each, in order. `fetch_subtree(node_id)`
1786/// marshals one block's subtree (local-indexed, root at 0) and is called *only*
1787/// for a block that missed the cache, i.e. one that actually changed. So a
1788/// keystroke marshals one small subtree, not ~20k nodes. The result is
1789/// byte-for-byte identical to [`build`] on the same document (the
1790/// `build_cached_matches_build` test pins this); [`build`] stays the cache-free,
1791/// whole-arena reference. This is the entry point [`crate::Doc`] uses.
1792// One builder, and every one of these is a distinct input to the same layout
1793// pass — a struct of them would be built at the one call site and unpacked
1794// here, which is the same arguments with an extra name in the way.
1795#[allow(clippy::too_many_arguments)]
1796pub fn build_cached(
1797    top: &[QueryMatch],
1798    source: &str,
1799    wrap: Option<usize>,
1800    preserve_soft: bool,
1801    surface: &Surface,
1802    reveal: Option<Reveal>,
1803    cache: &mut BlockCache,
1804    mut fetch_subtree: impl FnMut(u32) -> Vec<FlatNode>,
1805) -> VisualMap {
1806    let wrap = wrap.map(|w| w.max(8));
1807
1808    // Wrapping is a function of the width, so a width change makes every cached
1809    // row's wrap wrong: start the cache over.
1810    if cache.wrap != Some(wrap) {
1811        cache.entries.clear();
1812        cache.wrap = Some(wrap);
1813    }
1814    cache.generation = cache.generation.wrapping_add(1);
1815
1816    // Frontmatter (a leading `metadata` block) is document metadata, not prose:
1817    // hidden in the rich view exactly as [`Builder::blocks`] skips it.
1818    let blocks: Vec<&QueryMatch> = top.iter().filter(|m| m.kind != Kind::Metadata).collect();
1819
1820    // The outer builder only accumulates rows/tables and spells block boundaries
1821    // — both a function of the source and `last_off`, never of a node array — so
1822    // it carries an empty `nodes`. Each changed block is rendered by a *fresh*
1823    // builder over that block's subtree.
1824    let mut b = Builder {
1825        nodes: &[],
1826        source,
1827        wrap,
1828        rows: Vec::new(),
1829        tables: Vec::new(),
1830        last_off: 0,
1831        stepped_over: 0,
1832        surface,
1833        break_glyph: Cell::new(' '),
1834        preserve_soft,
1835        reveal: reveal.clone(),
1836        pending_mark_ends: RefCell::new(Vec::new()),
1837        pending_math: RefCell::new(Vec::new()),
1838        saw_math: Cell::new(false),
1839        presentation: Presentation::default(),
1840        faces: RefCell::new(FaceTable::default()),
1841    };
1842
1843    // Record the per-block row decomposition as we go, so a later
1844    // [`build_spliced`] can patch one block without rebuilding the map.
1845    let mut layout_blocks: Vec<BlockLayout> = Vec::with_capacity(blocks.len());
1846    // The document's face table, assembled block by block: from the walk on a
1847    // miss, from what the entry stored on a hit. The outer builder walks no
1848    // attributes of its own (it spells boundaries), so it never adds to it.
1849    let mut faces = FaceTable::default();
1850    let mut all_shift_safe = true;
1851    // The class of the last block that drew anything: what the next separator
1852    // closes, and what the trailing blank lines close at the end. A hidden block
1853    // (a comment) never becomes it — see [`Builder::block_or_hidden`], whose
1854    // step-over this loop repeats for the incremental walk.
1855    let mut above: Option<BlockClass> = None;
1856    let hidden_end = hidden_prefix_end(
1857        source,
1858        top.iter()
1859            .filter(|m| m.kind == Kind::Metadata)
1860            .map(|m| m.span.end)
1861            .next_back(),
1862    );
1863    for block in &blocks {
1864        let start = block.span.start;
1865        let before_sep = b.rows.len();
1866        // This walker has no node arena at all (see the `nodes: &[]` above),
1867        // but a top-level query match carries its kind — the same string
1868        // `BlockClass::from_node_kind` classifies for the whole-arena walk, so
1869        // the incremental and full builds label a boundary identically.
1870        let below = BlockClass::from_node_kind(&block.kind);
1871        if let Some(above) = above {
1872            b.emit_separators_before(start, &[], true, Boundary { above, below });
1873        } else {
1874            let from = b.leading_from(hidden_end);
1875            b.emit_leading_blank_lines(from, start, below);
1876        }
1877        let after_sep = b.rows.len();
1878        let bytes = block_bytes(source, &block.span);
1879        let hash = block_hash(bytes);
1880        // How this block meets the reveal line, if at all — part of its cache
1881        // key, since the same bytes render differently on the caret's line.
1882        let rkey = reveal_key(&reveal, &block.span);
1883
1884        // Hit: clone the block's rows shifted to its current offset and restore
1885        // the (shifted) `last_off` so the next separator lands right — no marshal.
1886        // Only shift-safe blocks are ever cached, so a hit is safe by construction.
1887        let mut has_math = false;
1888        if let Some(hit) = cache.reuse(hash, bytes, &rkey) {
1889            faces.merge(&hit.faces);
1890            has_math = hit.has_math;
1891            let delta = start as isize - hit.built_start as isize;
1892            for row in &hit.rows {
1893                b.rows.push(shift_row(row, delta));
1894            }
1895            b.last_off = (hit.last_off as isize + delta) as usize;
1896            // `0` is a block that stepped over nothing, and no answer to shift.
1897            if hit.stepped_over > 0 {
1898                let stepped = (hit.stepped_over as isize + delta) as usize;
1899                b.stepped_over = b.stepped_over.max(stepped);
1900            }
1901        } else {
1902            // Miss: marshal just this block's subtree and render it. A subtree is
1903            // self-contained with local ids (root at 0) and absolute spans, so a
1904            // fresh builder over it produces the same rows the whole-arena path
1905            // would. An empty subtree (twig couldn't hand it back) renders nothing.
1906            let subtree = fetch_subtree(block.node_id);
1907            if !subtree.is_empty() {
1908                let mut sub = Builder {
1909                    nodes: &subtree,
1910                    source,
1911                    wrap,
1912                    rows: Vec::new(),
1913                    tables: Vec::new(),
1914                    last_off: 0,
1915                    stepped_over: 0,
1916                    surface,
1917                    break_glyph: Cell::new(' '),
1918                    preserve_soft,
1919                    reveal: reveal.clone(),
1920                    pending_mark_ends: RefCell::new(Vec::new()),
1921                    pending_math: RefCell::new(Vec::new()),
1922                    saw_math: Cell::new(false),
1923                    presentation: Presentation::default(),
1924                    faces: RefCell::new(FaceTable::default()),
1925                };
1926                sub.block(0, &[], &[]);
1927                // A block that drew nothing is stepped over, not stood on: its
1928                // `last_off` is its own end, so the separator after it counts
1929                // from there. The sub-builder started at 0 and never moved, and
1930                // 0 is where the next separator would otherwise count from —
1931                // every line of the document, as a blank row each.
1932                let last_off = if sub.rows.is_empty() {
1933                    block.span.end
1934                } else {
1935                    sub.last_off
1936                };
1937                let stepped_over = sub.stepped_over;
1938                b.stepped_over = b.stepped_over.max(stepped_over);
1939                // The names this block's glyph ids stand for. They go into the
1940                // document's table *and* into the cache entry, because a hit
1941                // re-emits these rows without walking an attribute again.
1942                let block_faces = sub.faces.into_inner();
1943                faces.merge(&block_faces);
1944                has_math = sub.saw_math.get();
1945                // Cache only a block that is table-free AND renders inside its own
1946                // span: those two are the conditions for reuse-by-shift to be
1947                // correct. A block failing either is re-rendered every build (a
1948                // fresh render always matches a fresh whole-document build).
1949                if sub.tables.is_empty() {
1950                    if rows_within(&sub.rows, &block.span) {
1951                        cache.store(
1952                            hash,
1953                            bytes,
1954                            start,
1955                            sub.rows.clone(),
1956                            last_off,
1957                            stepped_over,
1958                            rkey,
1959                            has_math,
1960                            block_faces,
1961                        );
1962                    }
1963                    b.rows.extend(sub.rows);
1964                } else {
1965                    // A table block is never cached; rebase its row-index
1966                    // bookkeeping onto the combined row vector and append.
1967                    let base = b.rows.len();
1968                    for t in &mut sub.tables {
1969                        t.rows_span = (t.rows_span.start + base)..(t.rows_span.end + base);
1970                    }
1971                    b.rows.extend(sub.rows);
1972                    b.tables.extend(sub.tables);
1973                }
1974                b.last_off = last_off;
1975            }
1976        }
1977        let content_rows = b.rows.len() - after_sep;
1978        let sep_rows = if content_rows == 0 {
1979            // Hidden: take back the separator drawn for it, so what stands
1980            // either side meets across one boundary. Its layout entry stays, at
1981            // no rows, so the splice arithmetic still counts one entry per block.
1982            b.rows.truncate(before_sep);
1983            // A cache hit restored the stored `last_off` above; an empty subtree
1984            // (twig couldn't hand it back) restored nothing. Either way the walk
1985            // stands past the block.
1986            b.last_off = b.last_off.max(block.span.end);
1987            b.stepped_over = b.stepped_over.max(block.span.end);
1988            0
1989        } else {
1990            above = Some(BlockClass::from_node_kind(&block.kind));
1991            all_shift_safe &= rows_within(&b.rows[after_sep..], &block.span);
1992            after_sep - before_sep
1993        };
1994        layout_blocks.push(BlockLayout {
1995            span: block.span.clone(),
1996            kind: block.kind.clone(),
1997            sep_rows,
1998            content_rows,
1999            has_math,
2000        });
2001    }
2002
2003    let before_trailing = b.rows.len();
2004    b.emit_trailing_blank_lines(above.unwrap_or(BlockClass::Paragraph), hidden_end);
2005    let trailing_rows = b.rows.len() - before_trailing;
2006
2007    // Evict every entry no block reused this build, so the cache tracks the
2008    // current document instead of growing without bound over a session.
2009    let g = cache.generation;
2010    cache.entries.retain(|_, bucket| {
2011        bucket.retain(|e| e.generation == g);
2012        !bucket.is_empty()
2013    });
2014
2015    cache.layout = Layout {
2016        blocks: layout_blocks,
2017        trailing_rows,
2018        built_len: source.len(),
2019        has_tables: !b.tables.is_empty(),
2020        all_shift_safe,
2021        reveal: reveal.clone(),
2022    };
2023
2024    // The first rendered offset is the first non-metadata block's start — the
2025    // analogue of [`first_content_offset`] for the top-level list. With nothing
2026    // but frontmatter it's the end of that frontmatter, and 0 for an empty
2027    // document ([`hidden_prefix_end`]).
2028    let content_start = floor_of(&b.rows, blocks.first().map_or(hidden_end, |m| m.span.start));
2029    let stops = collect_stops(&b.rows);
2030    let mark_ends = collect_mark_ends(&b.rows);
2031    label_media_boundaries(&mut b.rows);
2032    let code_blocks = code_block_spans(&b.rows);
2033    let media = media_spans(&b.rows);
2034    let directives = directive_spans(&b.rows);
2035    let math = math_spans(&b.rows);
2036    VisualMap {
2037        rows: b.rows,
2038        content_start,
2039        stops,
2040        mark_ends,
2041        tables: b.tables,
2042        code_blocks,
2043        media,
2044        directives,
2045        math,
2046        faces,
2047        spelling: OnceLock::new(),
2048    }
2049}
2050
2051/// The fast path for a single-block edit: patch the previous [`VisualMap`] in
2052/// place rather than reassembling it. Returns `Some(new_map)` when it applies,
2053/// or `None` to tell the caller to fall back to [`build_cached`] (always
2054/// correct). Consumes `prev` either way — on `None` the caller rebuilds from
2055/// scratch and doesn't need it.
2056///
2057/// It applies only when `dirty` (twig's dirty byte range) falls inside exactly
2058/// one top-level block AND the block structure around it is unchanged — verified
2059/// by matching the new `top` list against the previous [`Layout`] block for
2060/// block: kinds unchanged, spans before the edit identical, spans after it
2061/// shifted by the byte delta, count unchanged. Any deviation — a block split or
2062/// merged, a fence opened to swallow later blocks, a table anywhere, a
2063/// multi-block edit — fails the match and returns `None`. That check is what
2064/// makes the byte-range trustworthy: twig's dirty range is exact about *bytes*
2065/// but silent about *reparse*, and the structural match catches the reparse
2066/// effects it can't see.
2067///
2068/// When it applies, the unchanged prefix rows move verbatim, the suffix rows
2069/// shift by the delta *in place* (integer adds, no glyph copy), and only the one
2070/// dirty block is re-marshalled and re-rendered; stops splice the same way by
2071/// offset. So the cost is O(rows after the edit), and nothing before the edit is
2072/// touched. The hash-keyed entry cache is left alone — a later [`build_cached`]
2073/// will miss on the changed block, re-render it, and evict the stale entry, so
2074/// chained splices neither corrupt nor grow it.
2075// One builder, and every one of these is a distinct input to the same layout
2076// pass — a struct of them would be built at the one call site and unpacked
2077// here, which is the same arguments with an extra name in the way.
2078#[allow(clippy::too_many_arguments)]
2079pub fn build_spliced(
2080    prev: VisualMap,
2081    source: &str,
2082    wrap: Option<usize>,
2083    preserve_soft: bool,
2084    top: &[QueryMatch],
2085    dirty: Range<usize>,
2086    surface: &Surface,
2087    reveal: Option<Reveal>,
2088    cache: &mut BlockCache,
2089    mut fetch_subtree: impl FnMut(u32) -> Vec<FlatNode>,
2090) -> Option<VisualMap> {
2091    let wrap = wrap.map(|w| w.max(8));
2092    // A width change invalidates every cached row — a full rebuild's job.
2093    if cache.wrap != Some(wrap) {
2094        return None;
2095    }
2096    // So does a moved reveal line, and for the same reason: this path reuses
2097    // every row outside the dirty block, and those rows encode which line was
2098    // showing its raw markup when they were built. Typing almost always moves
2099    // the caret, so under `MarkupMode::Full` this bails to `build_cached` on
2100    // most keystrokes — still block-cached, so only the edited block and the
2101    // revealed one actually re-render.
2102    if cache.layout.reveal != reveal {
2103        return None;
2104    }
2105    // Take the previous layout; on any bail below the caller rebuilds it (and the
2106    // map) via `build_cached`, so leaving it empty is fine. A table or a block
2107    // that renders outside its span (a degenerate inline span) makes shifting
2108    // unsound, so those force the full-rebuild path.
2109    let prev_layout = std::mem::take(&mut cache.layout);
2110    if prev_layout.built_len == 0 || prev_layout.has_tables || !prev_layout.all_shift_safe {
2111        return None;
2112    }
2113    // The layout addresses `prev` by row index, so it is only usable against the
2114    // map it was built from. A frontend is free to hold the map it was handed and
2115    // present it differently — leaf-ratatui splices blank filler rows under an
2116    // oversized heading so the raster has somewhere to stand — and if one of those
2117    // comes back here the row arithmetic below lands on the wrong rows: the
2118    // re-rendered block is laid over a filler and the rows it really occupied
2119    // survive into the suffix, stranding a stale copy of the edited line and
2120    // pushing everything after it one row down, once per keystroke. A row count
2121    // that doesn't match what this layout describes is the tell, and the honest
2122    // answer is the full rebuild.
2123    let described_rows = prev_layout
2124        .blocks
2125        .iter()
2126        .map(|pl| pl.sep_rows + pl.content_rows)
2127        .sum::<usize>()
2128        + prev_layout.trailing_rows;
2129    if described_rows != prev.rows.len() {
2130        return None;
2131    }
2132
2133    let blocks: Vec<&QueryMatch> = top.iter().filter(|m| m.kind != Kind::Metadata).collect();
2134    if blocks.is_empty() || blocks.len() != prev_layout.blocks.len() {
2135        return None;
2136    }
2137    let delta = source.len() as isize - prev_layout.built_len as isize;
2138
2139    // The single block whose NEW span contains the whole dirty range. A dirty
2140    // range straddling a block boundary (or a separator) finds none → bail.
2141    let k = blocks
2142        .iter()
2143        .position(|m| m.span.start <= dirty.start && dirty.end <= m.span.end)?;
2144
2145    // Structural match: every OTHER block is unchanged — same kind throughout,
2146    // span identical before the edit and shifted by `delta` after it. A mismatch
2147    // means the reparse reshaped the block structure, which only a full rebuild
2148    // renders correctly.
2149    for (i, (m, pl)) in blocks.iter().zip(&prev_layout.blocks).enumerate() {
2150        if m.kind != pl.kind {
2151            return None;
2152        }
2153        if i == k {
2154            continue;
2155        }
2156        let want = if i < k {
2157            pl.span.clone()
2158        } else {
2159            (pl.span.start as isize + delta) as usize..(pl.span.end as isize + delta) as usize
2160        };
2161        if m.span != want {
2162            return None;
2163        }
2164    }
2165    // The dirty block itself: start unchanged (the edit is inside it, past its
2166    // start), end moved by exactly the delta.
2167    let pk_start = prev_layout.blocks[k].span.start;
2168    let pk_end = prev_layout.blocks[k].span.end;
2169    let pk_sep = prev_layout.blocks[k].sep_rows;
2170    let pk_content = prev_layout.blocks[k].content_rows;
2171    if blocks[k].span.start != pk_start || blocks[k].span.end != (pk_end as isize + delta) as usize
2172    {
2173        return None;
2174    }
2175
2176    // Re-render the dirty block from its subtree. A table makes the splice
2177    // bookkeeping unsafe, so bail if one appears.
2178    let subtree = fetch_subtree(blocks[k].node_id);
2179    if subtree.is_empty() {
2180        return None;
2181    }
2182    let mut sub = Builder {
2183        nodes: &subtree,
2184        source,
2185        wrap,
2186        rows: Vec::new(),
2187        tables: Vec::new(),
2188        last_off: 0,
2189        stepped_over: 0,
2190        surface,
2191        break_glyph: Cell::new(' '),
2192        preserve_soft,
2193        reveal: reveal.clone(),
2194        pending_mark_ends: RefCell::new(Vec::new()),
2195        pending_math: RefCell::new(Vec::new()),
2196        saw_math: Cell::new(false),
2197        presentation: Presentation::default(),
2198        faces: RefCell::new(FaceTable::default()),
2199    };
2200    sub.block(0, &[], &[]);
2201    // A table, or content that renders outside the block's span (a degenerate
2202    // inline span), makes the shift bookkeeping unsound — fall back.
2203    if !sub.tables.is_empty() || !rows_within(&sub.rows, &blocks[k].span) {
2204        return None;
2205    }
2206    // The face table starts from the previous map's, because every row this
2207    // path keeps was built against it and its glyphs' ids still mean what they
2208    // meant. The re-rendered block adds whatever it met. An id the edit took
2209    // the last glyph of stays in the table, naming nothing — the price of not
2210    // walking the rows this path exists to avoid walking.
2211    let mut faces = prev.faces;
2212    faces.merge(&sub.faces.into_inner());
2213    let sub_saw_math = sub.saw_math.get();
2214    let new_content = sub.rows;
2215    let new_content_len = new_content.len();
2216    let new_stops = collect_stops(&new_content);
2217    let new_mark_ends = collect_mark_ends(&new_content);
2218
2219    // Row span of the dirty block's CONTENT. Its leading separator stays in the
2220    // prefix: the gap before block k is unchanged, since k's start didn't move.
2221    let content_start_row: usize = prev_layout.blocks[..k]
2222        .iter()
2223        .map(|pl| pl.sep_rows + pl.content_rows)
2224        .sum::<usize>()
2225        + pk_sep;
2226    let content_end_row = content_start_row + pk_content;
2227
2228    // Splice rows: [prefix | new content | suffix + delta]. The prefix moves
2229    // untouched; the suffix shifts in place — integer adds, no glyph copy.
2230    let mut rows = prev.rows;
2231    let mut suffix = rows.split_off(content_end_row);
2232    rows.truncate(content_start_row);
2233    for row in &mut suffix {
2234        shift_row_in_place(row, delta);
2235    }
2236    rows.reserve(new_content_len + suffix.len());
2237    rows.extend(new_content);
2238    rows.extend(suffix);
2239
2240    // Splice stops by offset. The old dirty block covered `[pk_start, pk_end]`:
2241    // prefix stops fall below it, suffix stops above it (shift by delta), the new
2242    // content supplies the middle. The three ranges stay disjoint and ascending,
2243    // so the result needs no re-sort.
2244    let p1 = prev.stops.partition_point(|&s| s < pk_start);
2245    let p2 = prev.stops.partition_point(|&s| s <= pk_end);
2246    let mut stops = Vec::with_capacity(p1 + new_stops.len() + (prev.stops.len() - p2));
2247    stops.extend_from_slice(&prev.stops[..p1]);
2248    stops.extend(new_stops);
2249    for &s in &prev.stops[p2..] {
2250        stops.push((s as isize + delta) as usize);
2251    }
2252    // The mark ends splice the same way: they are offsets in the same
2253    // coordinates, cut at the same block.
2254    let m1 = prev.mark_ends.partition_point(|&s| s < pk_start);
2255    let m2 = prev.mark_ends.partition_point(|&s| s <= pk_end);
2256    let mut mark_ends = Vec::with_capacity(m1 + new_mark_ends.len() + (prev.mark_ends.len() - m2));
2257    mark_ends.extend_from_slice(&prev.mark_ends[..m1]);
2258    mark_ends.extend(new_mark_ends);
2259    for &s in &prev.mark_ends[m2..] {
2260        mark_ends.push((s as isize + delta) as usize);
2261    }
2262
2263    // Record the patched layout for the next splice: spans move to the new
2264    // coordinates, and the dirty block takes its new content-row count.
2265    let mut new_blocks = prev_layout.blocks;
2266    for (pl, m) in new_blocks.iter_mut().zip(&blocks) {
2267        pl.span = m.span.clone();
2268    }
2269    new_blocks[k].content_rows = new_content_len;
2270    new_blocks[k].has_math = sub_saw_math;
2271    cache.layout = Layout {
2272        blocks: new_blocks,
2273        trailing_rows: prev_layout.trailing_rows,
2274        built_len: source.len(),
2275        has_tables: false,
2276        // Every prefix/suffix block was shift-safe last build (we bailed
2277        // otherwise) and the re-rendered block was just checked, so the patched
2278        // document is still entirely shift-safe.
2279        all_shift_safe: true,
2280        reveal,
2281    };
2282
2283    label_media_boundaries(&mut rows);
2284    let code_blocks = code_block_spans(&rows);
2285    let media = media_spans(&rows);
2286    let directives = directive_spans(&rows);
2287    let math = math_spans(&rows);
2288    Some(VisualMap {
2289        rows,
2290        // The edit was inside one block, so no block moved its start and the
2291        // lines above the first are the ones the floor was taken from.
2292        content_start: prev.content_start,
2293        stops,
2294        mark_ends,
2295        tables: Vec::new(),
2296        code_blocks,
2297        media,
2298        directives,
2299        math,
2300        faces,
2301        spelling: OnceLock::new(),
2302    })
2303}
2304
2305/// A persistent, content-keyed cache of the rows each top-level block renders
2306/// to — the [`VisualMap`] analogue of the GUI's ShapedLine cache, one level
2307/// down. Held by a [`crate::Doc`] and threaded into [`build_cached`], it is what
2308/// makes a rebuild after a keystroke cost "re-render the edited block + shift
2309/// the rest" instead of re-rendering the whole document.
2310///
2311/// A top-level block's rows are a pure function of its source bytes and the wrap
2312/// width, so an unchanged block's rows are cloned and their source offsets
2313/// shifted by the edit's byte delta rather than rebuilt glyph by glyph. Two
2314/// things make that purity hold: at the top level the render prefix is always
2315/// empty (nesting prefixes — a quote gutter, a list indent — exist only *inside*
2316/// a top-level block, within its cached unit), and a block's output never reads
2317/// the incoming `last_off` (it writes `last_off` from its own content before any
2318/// nested separator reads it). So the only thing that differs between two
2319/// positions of an unchanged block is a uniform offset shift. Keyed by a fast
2320/// hash of the block's bytes with the bytes kept for a verify-on-hit — exactly
2321/// the shape cache's weak-hash-then-compare, so a collision costs a re-render,
2322/// never a wrong row.
2323///
2324/// Tables are never cached (a block that emits any table row is always rebuilt):
2325/// their rows are cross-referenced from the map's `tables` side-table by row
2326/// index, which a blind offset-shift wouldn't fix up, and they are rare enough
2327/// that the simplicity beats the reuse.
2328#[derive(Default)]
2329pub struct BlockCache {
2330    /// The wrap width every entry was built at; a change invalidates all of
2331    /// them. `None` before the first build (distinct from `Some(None)`, the
2332    /// unwrapped GUI width).
2333    wrap: Option<Option<usize>>,
2334    /// Bumped once per [`build_cached`]. An entry reused or inserted this build
2335    /// carries the current value; stale entries are dropped at the end of it.
2336    generation: u64,
2337    /// `hash(bytes)` → the block(s) sharing that hash — a bucket because
2338    /// distinct blocks can collide, while two *identical* blocks share one entry
2339    /// (free dedup).
2340    entries: HashMap<u64, Vec<CachedBlock>>,
2341    /// The row/stop decomposition of the last build, which [`build_spliced`]
2342    /// patches in place for a single-block edit. Kept in step with whatever
2343    /// [`VisualMap`] was last produced; empty before the first build.
2344    layout: Layout,
2345}
2346
2347/// How the last build's [`VisualMap`] decomposes into top-level blocks — the
2348/// bookkeeping [`build_spliced`] needs to splice one block's rows and stops
2349/// without rebuilding the whole map. Every field describes the *previous* build,
2350/// in that build's coordinates.
2351#[derive(Default)]
2352struct Layout {
2353    /// One entry per rendered (metadata-filtered) top-level block, in order.
2354    blocks: Vec<BlockLayout>,
2355    /// Trailing blank rows past the last block (from `emit_trailing_blank_lines`).
2356    trailing_rows: usize,
2357    /// The source length this layout was built at — the reference for the edit's
2358    /// byte delta.
2359    built_len: usize,
2360    /// Whether the last build drew any table. A table's cross-referenced row
2361    /// indices don't survive a blind splice, so their presence makes
2362    /// [`build_spliced`] bail to a full rebuild.
2363    has_tables: bool,
2364    /// Whether every block rendered strictly inside its own span (see
2365    /// [`rows_within`]). A block that doesn't — a malformed Markdown inline node
2366    /// that twig leaves with a degenerate `0..0` span renders at a fixed offset
2367    /// outside its block — can't be shifted correctly, so its presence makes
2368    /// [`build_spliced`] bail to a full rebuild.
2369    all_shift_safe: bool,
2370    /// The reveal line this layout was built under (see [`Builder::reveal`]).
2371    /// A splice reuses every row it isn't re-rendering, so a reveal line that
2372    /// has moved would leave the old line still showing its delimiters and the
2373    /// new one still hiding them — [`build_spliced`] bails when this changes.
2374    reveal: Option<Reveal>,
2375}
2376
2377/// One top-level block's contribution to the last build: its span and kind (for
2378/// the structural match that proves only one block changed) and how many
2379/// separator and content rows it emitted (to locate its slice of the row
2380/// vector).
2381struct BlockLayout {
2382    span: Range<usize>,
2383    kind: Kind,
2384    sep_rows: usize,
2385    content_rows: usize,
2386    /// Whether the block holds a formula — see [`BlockCache::math_meets`].
2387    has_math: bool,
2388}
2389
2390/// One cached block: the rows it rendered to, plus what a reuse at a new
2391/// position needs to shift them. Offsets are stored absolute (as built) and
2392/// shifted by `new_start - built_start` on reuse.
2393struct CachedBlock {
2394    /// The block's exact source bytes, compared on a hash hit so a collision
2395    /// can never hand back another block's rows.
2396    bytes: Box<[u8]>,
2397    /// The offset the rows were built at (the block's `span.start`).
2398    built_start: usize,
2399    /// The block's rows, offsets absolute as built.
2400    rows: Vec<VRow>,
2401    /// `last_off` after this block was emitted, absolute as built — restored
2402    /// (shifted) on reuse so the following separator lands correctly.
2403    last_off: usize,
2404    /// `stepped_over` after this block was emitted, absolute as built — the
2405    /// hidden tail a block ends with (a `</div>`), restored with `last_off`
2406    /// so the trailing blank lines are counted from past it on a hit too.
2407    stepped_over: usize,
2408    /// Where the reveal line fell *within this block* when the rows were built,
2409    /// as a block-relative byte range — see [`reveal_key`]. Compared alongside
2410    /// `bytes` on a hit, because identical source renders to different rows
2411    /// depending on whether the caret's line is inside it: the same `*em*`
2412    /// shows its asterisks on the revealed line and hides them everywhere else.
2413    ///
2414    /// Block-relative rather than absolute so an unaffected block still hits
2415    /// after an edit shifts it, and `None` for the overwhelmingly common
2416    /// no-reveal case — which is why an entry stored under `MarkupMode::None`
2417    /// keeps hitting for every block that isn't the caret's.
2418    reveal: Option<Range<usize>>,
2419    /// Whether the block holds a formula, so a hit can say so to the layout
2420    /// without walking the rows it is re-emitting.
2421    has_math: bool,
2422    /// The named families this block's glyphs are set in — see
2423    /// [`VisualMap::faces`]. Stored with the rows because a hit re-emits them
2424    /// without walking a `data-font` again, and the map still has to be able to
2425    /// say what the id on a reused glyph names. Empty for every block that
2426    /// names no family, which is nearly all of them.
2427    faces: FaceTable,
2428    /// The build that last reused or inserted this entry (see `generation`).
2429    generation: u64,
2430}
2431
2432/// Where `reveal` falls inside a block, in block-relative bytes — the extra key
2433/// a cached block is stored and matched under.
2434///
2435/// `None` when the block doesn't meet the reveal line at all, which is every
2436/// block on every build in the two hidden modes, and all but one of them under
2437/// [`crate::MarkupMode::Full`]. So the cache keeps its hit rate as the caret
2438/// moves: only the line the caret leaves and the line it arrives at re-render.
2439fn reveal_key(reveal: &Option<Reveal>, span: &Range<usize>) -> Option<Range<usize>> {
2440    let r = reveal.as_ref()?;
2441    // The same generous intersection test `Builder::revealed` uses, so a block
2442    // is keyed as revealed exactly when its glyphs will be built that way.
2443    // Whether the line reveals markup or only math is not in the key: that is
2444    // a mode, and a mode change empties the cache.
2445    r.meets(span).then(|| {
2446        let start = r.line.start.max(span.start) - span.start;
2447        let end = r.line.end.min(span.end) - span.start;
2448        start..end
2449    })
2450}
2451
2452impl BlockCache {
2453    /// Whether the caret's `line` meets a top-level block that holds a
2454    /// formula, as of the last build — the question [`crate::Doc::reveal_line`]
2455    /// asks in the hidden markup modes before it threads the line through, so
2456    /// that only a line with something to reveal costs a rebuild.
2457    ///
2458    /// Answered from the layout rather than from twig because the layout is
2459    /// free: every build records per block whether its walk met a formula
2460    /// ([`BlockLayout::has_math`]), and a query against the tree is
2461    /// O(document) on every frame. Coarse on purpose — a block, not a line —
2462    /// since a block with a formula in it is rare, small, and the one thing
2463    /// worth re-rendering as the caret moves through it.
2464    ///
2465    /// Exact between edits, when the spans are the document's. Across an edit
2466    /// the spans are the previous revision's, so the caller checks again once
2467    /// the new layout is in and rebuilds if the answer changed.
2468    pub(crate) fn math_meets(&self, line: &Range<usize>) -> bool {
2469        self.layout
2470            .blocks
2471            .iter()
2472            .any(|b| b.has_math && b.span.start <= line.end && line.start <= b.span.end)
2473    }
2474
2475    /// Look up a block by hash, verify its bytes and reveal key, and on a hit
2476    /// stamp it used this build and hand back a borrow to shift-and-clone from.
2477    /// `None` on a miss (unknown hash, a collision whose bytes differ, or the
2478    /// same bytes built under a different reveal).
2479    fn reuse(
2480        &mut self,
2481        hash: u64,
2482        bytes: &[u8],
2483        reveal: &Option<Range<usize>>,
2484    ) -> Option<&CachedBlock> {
2485        let g = self.generation;
2486        let bucket = self.entries.get_mut(&hash)?;
2487        let e = bucket
2488            .iter_mut()
2489            .find(|e| &*e.bytes == bytes && &e.reveal == reveal)?;
2490        e.generation = g;
2491        Some(&*e)
2492    }
2493
2494    /// Cache the rows a freshly-rendered block produced (or refresh an existing
2495    /// entry for the same bytes and reveal — an identical block elsewhere, or a
2496    /// re-render).
2497    #[allow(clippy::too_many_arguments)]
2498    fn store(
2499        &mut self,
2500        hash: u64,
2501        bytes: &[u8],
2502        built_start: usize,
2503        rows: Vec<VRow>,
2504        last_off: usize,
2505        stepped_over: usize,
2506        reveal: Option<Range<usize>>,
2507        has_math: bool,
2508        faces: FaceTable,
2509    ) {
2510        let g = self.generation;
2511        let bucket = self.entries.entry(hash).or_default();
2512        if let Some(e) = bucket
2513            .iter_mut()
2514            .find(|e| &*e.bytes == bytes && e.reveal == reveal)
2515        {
2516            e.built_start = built_start;
2517            e.rows = rows;
2518            e.last_off = last_off;
2519            e.stepped_over = stepped_over;
2520            e.has_math = has_math;
2521            e.faces = faces;
2522            e.generation = g;
2523        } else {
2524            bucket.push(CachedBlock {
2525                bytes: bytes.into(),
2526                built_start,
2527                rows,
2528                last_off,
2529                stepped_over,
2530                reveal,
2531                has_math,
2532                faces,
2533                generation: g,
2534            });
2535        }
2536    }
2537}
2538
2539/// The source bytes a top-level block covers — the block cache's key material.
2540///
2541/// Clamped to the source rather than sliced by the span as twig gives it,
2542/// because that span can end *past* the last byte: the final block of a document
2543/// with no trailing newline is closed on the virtual newline the parser supplies
2544/// at EOF, so its `span.end` is `source.len() + 1`. Slicing by such a range
2545/// yields `None`, and the obvious `unwrap_or(&[])` reads that as *this block has
2546/// no bytes* — the wrong answer twice over.
2547///
2548/// Two blocks whose spans both overrun then key alike, and the second is served
2549/// the first one's rows. That is not hypothetical: a footnote definition is a
2550/// root beside `doc` merged back into the top level by [`top_blocks`], while the
2551/// `section` above it spans the definition's bytes too, so both end at EOF —
2552/// and a document ending in `[^note]: …` renders that definition as a second
2553/// copy of the heading. Even alone, a block that keeps hashing empty as the user
2554/// types in it is served the stale rows built before the edit.
2555///
2556/// Clamping hands back the bytes the block really covers, which tells both cases
2557/// apart, and costs nothing for a span that was in range to begin with.
2558fn block_bytes<'a>(source: &'a str, span: &Range<usize>) -> &'a [u8] {
2559    let bytes = source.as_bytes();
2560    let start = span.start.min(bytes.len());
2561    &bytes[start..span.end.clamp(start, bytes.len())]
2562}
2563
2564/// A fast, allocation-free content hash (FNV-1a) for a block's bytes. Weak by
2565/// design — the bytes are compared on a hit — so its only job is to spread
2566/// blocks across buckets cheaply. SipHash over every block's bytes on every
2567/// keystroke would cost more than it saves, the same lesson the shape cache
2568/// learned when it stopped hashing through the standard hasher.
2569fn block_hash(bytes: &[u8]) -> u64 {
2570    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
2571    for &x in bytes {
2572        h ^= x as u64;
2573        h = h.wrapping_mul(0x0000_0100_0000_01b3);
2574    }
2575    h
2576}
2577
2578/// Clone a cached row with every source offset advanced by `delta` — the whole
2579/// cost of reusing an unchanged block: integer adds where a rebuild would
2580/// re-shape every glyph.
2581fn shift_row(row: &VRow, delta: isize) -> VRow {
2582    let shift = |off: usize| (off as isize + delta) as usize;
2583    VRow {
2584        glyphs: row
2585            .glyphs
2586            .iter()
2587            .map(|g| Glyph {
2588                ch: g.ch,
2589                style: g.style,
2590                src: shift(g.src),
2591                stop: g.stop,
2592            })
2593            .collect(),
2594        end_src: shift(row.end_src),
2595        decoration: row.decoration,
2596        code: row.code,
2597        code_lang: row.code_lang.clone(),
2598        directive: row.directive,
2599        directive_label: row.directive_label.clone(),
2600        media: row.media.clone(),
2601        // A tick, not an offset — reuse carries it as-is, like `code_lang`.
2602        task: row.task,
2603        leaf_directive: row.leaf_directive.clone(),
2604        heading: row.heading,
2605        // Presentation, not offsets: names the author wrote, which a shifted
2606        // block still wears — like `code_lang`.
2607        align: row.align,
2608        line_height: row.line_height,
2609        // Structure, not offsets: a reused block's rows divide the same blocks
2610        // wherever the edit above moved them to.
2611        boundary: row.boundary,
2612        mark_ends: row.mark_ends.iter().map(|&o| shift(o)).collect(),
2613        // Strings and a glyph index: the glyph moved, the index into the row
2614        // did not.
2615        math: row.math.clone(),
2616    }
2617}
2618
2619/// Advance a row's source offsets by `delta` in place — the suffix half of
2620/// [`build_spliced`], where the rows are already owned and only need shifting,
2621/// not copying.
2622fn shift_row_in_place(row: &mut VRow, delta: isize) {
2623    for g in &mut row.glyphs {
2624        g.src = (g.src as isize + delta) as usize;
2625    }
2626    row.end_src = (row.end_src as isize + delta) as usize;
2627    for o in &mut row.mark_ends {
2628        *o = (*o as isize + delta) as usize;
2629    }
2630}
2631
2632/// Whether every source offset a block's rows carry falls inside the block's own
2633/// span — the precondition for reusing the block by a uniform offset shift. It
2634/// holds for well-formed blocks (their glyphs and row ends address bytes within
2635/// the block, synthetic glyphs point at the block start). It fails when a node
2636/// renders *outside* its block, which today means a malformed Markdown inline
2637/// node twig leaves with a degenerate `0..0` span: that content lands at a fixed
2638/// offset that doesn't move with the block. Such a block is re-rendered every
2639/// build instead of shifted, so the incremental map still matches a fresh one —
2640/// see [`build_cached`] and [`build_spliced`].
2641fn rows_within(rows: &[VRow], span: &Range<usize>) -> bool {
2642    rows.iter().all(|r| {
2643        r.end_src >= span.start
2644            && r.end_src <= span.end
2645            && r.glyphs
2646                .iter()
2647                .all(|g| g.src >= span.start && g.src <= span.end)
2648    })
2649}
2650
2651/// Where the rendered document begins when a leading `metadata` block is all
2652/// there is — the end of that hidden frontmatter, past the newline that closes
2653/// its last line so the floor sits at the start of the (empty) body rather than
2654/// on the closing `---`.
2655///
2656/// With a real block after it the frontmatter's end is never needed: the floor
2657/// is that block's start, and the rows begin there. With nothing after it, both
2658/// the caret floor and the trailing-blank-line count would otherwise fall back
2659/// to offset 0 — inside the hidden frontmatter — which put the caret *before*
2660/// the metadata and made typing land ahead of the opening `---`.
2661fn hidden_prefix_end(source: &str, meta_end: Option<usize>) -> usize {
2662    let Some(end) = meta_end else { return 0 };
2663    let end = end.min(source.len());
2664    let rest = &source[end..];
2665    if rest.starts_with("\r\n") {
2666        end + 2
2667    } else if rest.starts_with('\n') {
2668        end + 1
2669    } else {
2670        end
2671    }
2672}
2673
2674/// The caret floor: the first block's start, or the first blank line above it
2675/// when those lines draw rows ([`Builder::emit_leading_blank_lines`]).
2676fn floor_of(rows: &[VRow], first_start: usize) -> usize {
2677    rows.first()
2678        .map_or(first_start, |r| r.end_src.min(first_start))
2679}
2680
2681/// The end of the document's hidden frontmatter: the last `metadata` child of
2682/// `doc`, which is what [`top_level`] and [`Builder::blocks`] drop. `None` when
2683/// there is none.
2684fn metadata_end_of(nodes: &[FlatNode], doc: usize) -> Option<usize> {
2685    let mut end = None;
2686    let mut child = nodes[doc].first_child;
2687    while let Some(cid) = child {
2688        let n = &nodes[cid.0 as usize];
2689        if n.kind == Kind::Metadata {
2690            end = Some(n.span.end);
2691        }
2692        child = n.next_sibling;
2693    }
2694    end
2695}
2696
2697/// The document's rendered top-level blocks, as node indices in source order.
2698///
2699/// Not simply `doc`'s children, for two reasons. Frontmatter (a leading
2700/// `metadata` block) is document metadata rather than prose and is dropped, the
2701/// way [`Builder::blocks`] drops it. And a **footnote definition** (`[^1]: …`)
2702/// is not a child of `doc` at all: twig parses it as a root of its own, a
2703/// *sibling* of the document node with `parent == None`. A walk that starts at
2704/// `doc` therefore never reaches one, which is why a definition — and every
2705/// byte of its body — used to render as nothing at all. Merging the roots back
2706/// in by `span.start` puts each definition on screen exactly where it was
2707/// written, which is what keeps rows, stops, and offsets monotonic.
2708///
2709/// A **link reference definition** (`[foo]: /url`) is a root of the same kind,
2710/// and is merged for the opposite reason: it draws *nothing*, and the walk has
2711/// to know where it stands to step over it. A definition closing a README —
2712/// the `[links]: …` block under the prose — left no block over its lines, so
2713/// the separator logic read them as blank lines and drew an empty paragraph
2714/// per definition. Merged in, it is a hidden block like a comment, and
2715/// [`Builder::block_or_hidden`] moves the walk past it. One with no span
2716/// (`0..0`, what twig before 3.3.3 reported for every one) has nowhere to be
2717/// merged, and is left out as before.
2718///
2719/// Only those roots are merged. twig also leaves stray orphan `str` nodes
2720/// parented to nothing (the `*` of an emphasis run, for one); those are already
2721/// rendered as part of the subtree that owns their bytes, and re-emitting them
2722/// here would double them.
2723fn top_level(nodes: &[FlatNode], doc: usize) -> Vec<usize> {
2724    let mut out = Vec::new();
2725    let mut child = nodes[doc].first_child;
2726    while let Some(cid) = child {
2727        let n = &nodes[cid.0 as usize];
2728        if n.kind != Kind::Metadata {
2729            out.push(cid.0 as usize);
2730        }
2731        child = n.next_sibling;
2732    }
2733    out.extend(
2734        nodes
2735            .iter()
2736            .enumerate()
2737            .filter(|(_, n)| n.parent.is_none() && is_placed_definition(&n.kind, &n.span))
2738            .map(|(i, _)| i),
2739    );
2740    out.sort_by_key(|&i| nodes[i].span.start);
2741    out
2742}
2743
2744/// Is a parentless node of `kind` at `span` a definition the top-level walk
2745/// merges in — a footnote definition, or a link reference definition that
2746/// knows where it stands? Shared by [`top_level`] and [`top_blocks`] so the
2747/// two walks cannot disagree about what the top-level blocks are.
2748fn is_placed_definition(kind: &Kind, span: &Range<usize>) -> bool {
2749    match *kind {
2750        Kind::Footnote => true,
2751        Kind::Reference => span.end > span.start,
2752        _ => false,
2753    }
2754}
2755
2756/// The top-level blocks to hand [`build_cached`] / [`build_spliced`] — the
2757/// incremental path's twin of [`top_level`], which the two must agree with block
2758/// for block or the render paths diverge.
2759///
2760/// `child_spans(None)` gives `doc`'s children, which is all of them for an
2761/// ordinary document. A **footnote definition** is not one: twig parses `[^1]: …`
2762/// as a root beside `doc` with no parent, and indexes it at no offset either —
2763/// `node_at` inside its bytes answers `doc`, and a `query("footnote")` selector
2764/// finds nothing. Leaf used to discover them by marshalling the whole arena with
2765/// `nodes()` — the very cost the incremental path exists to avoid — behind a
2766/// byte-scan gate that gave documents with no `[^…]:` line a substring search
2767/// instead. twig 3.0's `definitions()` asks the library the question directly,
2768/// so both the marshal and the gate are gone.
2769///
2770/// The link reference definitions `definitions()` also reports are merged on
2771/// the same terms as [`top_level`] merges them — see [`is_placed_definition`].
2772///
2773/// This is the one part of the render that needs an [`Editor`] rather than a
2774/// marshalled node array. The builders themselves stay editor-free; this only
2775/// prepares their input.
2776pub(crate) fn top_blocks(editor: &mut Editor) -> Vec<QueryMatch> {
2777    let mut top = editor.child_spans(None).unwrap_or_default();
2778    let defs: Vec<QueryMatch> = definitions(editor)
2779        .into_iter()
2780        .filter(|m| is_placed_definition(&m.kind, &m.span))
2781        .collect();
2782    if defs.is_empty() {
2783        return top;
2784    }
2785    top.extend(defs);
2786    // Source order — what every offset-keyed thing downstream (rows, stops, the
2787    // splice path's block-for-block match) is built to assume.
2788    top.sort_by_key(|m| m.span.start);
2789    top
2790}
2791
2792/// Every `[^label]: …` definition in the document, in whatever order twig
2793/// reports them.
2794///
2795/// Filtered to [`Kind::Footnote`]: `definitions()` also reports the *link*
2796/// reference definitions (`[foo]: /url`), which are [`top_blocks`]'s business
2797/// and not [`crate::Doc::footnote_at_caret`]'s.
2798///
2799/// Empty when the document can't be walked, which leaves [`top_blocks`] with
2800/// the ordinary top-level children and [`crate::Doc::footnote_at_caret`] with an
2801/// undefined reference — in both cases the same answer as a document that has
2802/// no definitions, which is the right way to degrade.
2803pub(crate) fn footnote_definitions(editor: &mut Editor) -> Vec<QueryMatch> {
2804    definitions(editor)
2805        .into_iter()
2806        .filter(|m| m.kind == Kind::Footnote)
2807        .collect()
2808}
2809
2810/// Every definition twig resolves by label rather than by position — footnote
2811/// and link reference definitions both — or nothing when the document can't be
2812/// walked.
2813fn definitions(editor: &mut Editor) -> Vec<QueryMatch> {
2814    let Ok(mut doc) = editor.document() else {
2815        return Vec::new();
2816    };
2817    doc.definitions().unwrap_or_default()
2818}
2819
2820/// The last line of a block whose span ends at `span_end` — a thematic break,
2821/// a page break — as the two offsets the rest of the crate means by it: where
2822/// the line's text ends, and the home past it — past the newline that ends the
2823/// line, the start of the line under it (or the text's end, with no newline to
2824/// pass). A rule's row ends at that home.
2825///
2826/// Read off the span less the newline djot's span takes in, since Markdown's
2827/// stops before it. Measured from djot's span end, the home after a rule landed
2828/// past the blank line under it, on the next block's first character.
2829pub(crate) fn block_line(source: &str, span_end: usize) -> (usize, usize) {
2830    let span_end = span_end.min(source.len());
2831    let text_end = source[..span_end]
2832        .strip_suffix('\n')
2833        .map_or(span_end, |s| s.strip_suffix('\r').unwrap_or(s).len());
2834    let rest = &source[text_end..];
2835    let newline = if rest.starts_with("\r\n") {
2836        2
2837    } else {
2838        usize::from(rest.starts_with('\n'))
2839    };
2840    (text_end, text_end + newline)
2841}
2842
2843/// The label of the footnote definition starting at `start` — the `1` in
2844/// `[^1]: …`. twig gives the `footnote` node no label of its own (no `text`, no
2845/// `name`), and the bytes that spell it belong to no child node either — the
2846/// body `para` starts its *content* past them — so the source is the only place
2847/// to read it from. `None` when what's there isn't a definition after all.
2848pub(crate) fn footnote_label(source: &str, start: usize) -> Option<&str> {
2849    let rest = source.get(start..)?.strip_prefix("[^")?;
2850    let end = rest.find("]:")?;
2851    Some(&rest[..end])
2852}
2853
2854/// Where the body of the footnote definition spanning `span` sits in `source` —
2855/// everything past the `[^1]:` marker, which is the part a reader actually wants
2856/// when they follow a reference.
2857///
2858/// Source bytes, verbatim but for the whitespace trimmed off each end: a note
2859/// that says `see *later*` answers with the asterisks in. Rendering that body is
2860/// a frontend's business the same way painting a [`Role`] is, and a caller that
2861/// wants it laid out already has the definition on screen where it was written.
2862///
2863/// The trim is what makes the common case read right — `[^1]: text` has a space
2864/// after the colon that belongs to the marker, not the note, and a definition's
2865/// span runs to the newline ending it.
2866///
2867/// The span is taken at its word, which it has only been safe to do since twig
2868/// 3.1: a djot definition's span used to run *past* its own last line, through
2869/// the blank line separating it from the next block and into that block's first
2870/// byte, so `[^2a]: a note.` came back as `"a note.\n\n["` and the offsets named
2871/// the following note's rows as well as this one's — a reader asking about one
2872/// footnote was shown two. leaf measured the body itself to get around that, and
2873/// paid for it: the scan stopped at the first blank line, so a note with a second
2874/// indented paragraph lost it. Both halves go away with the fix, since a blank
2875/// line *inside* a definition was always interior to the span and still is.
2876///
2877/// A range rather than a slice because "go to note" needs the *position* as much
2878/// as the text, and it needs the position of the body specifically: a
2879/// definition's `[^1]:` marker is decoration the caret can't occupy (the rich
2880/// view draws it as `[1] ` and gives it no stop), so aiming a caret at the
2881/// definition's first byte lands it on the nearest real stop instead — which is
2882/// up in the paragraph *above* the note. The body's first byte is a stop, and is
2883/// where a reader following a reference wants to arrive anyway.
2884pub(crate) fn footnote_body_span(source: &str, span: Range<usize>) -> Option<Range<usize>> {
2885    let rest = source.get(span.clone())?.strip_prefix("[^")?;
2886    let marker = rest.find("]:")?;
2887    // `span.start` + `[^` + the label + `]:`.
2888    let after_marker = span.start + 2 + marker + 2;
2889    let raw = source.get(after_marker..span.end)?;
2890    // Written as a start plus a length so an all-whitespace body lands on an
2891    // empty range at the end rather than an inverted one.
2892    let start = after_marker + (raw.len() - raw.trim_start().len());
2893    Some(start..start + raw.trim().len())
2894}
2895
2896/// The label of the footnote *reference* spanning `span` — the `1` in `[^1]`.
2897///
2898/// The peer of [`footnote_label`] for the other half of the pair, and needed for
2899/// the same reason: a reference whose node carries neither a `content_span` nor
2900/// a `text` still spells its label plainly in the source. `None` when the bytes
2901/// aren't a reference after all.
2902pub(crate) fn footnote_reference_label(source: &str, span: Range<usize>) -> Option<&str> {
2903    let rest = source.get(span)?.strip_prefix("[^")?;
2904    let end = rest.find(']')?;
2905    Some(&rest[..end])
2906}
2907
2908/// Where a heading's *content* starts — past the `#`s and the space the rich
2909/// view hides, for an ATX heading; the block's own start for a setext one (which
2910/// has no leading marker) and for a format that spells headings some other way.
2911///
2912/// Only an empty heading needs asking: with any content at all, the row ends on
2913/// its last glyph. Bounded to the heading's own first line so a marker-less
2914/// heading can't scan into the text under it.
2915fn heading_content_start(source: &str, span: &Range<usize>) -> usize {
2916    let end = span.end.min(source.len());
2917    let Some(line) = source.get(span.start..end) else {
2918        return span.start;
2919    };
2920    let line = line.split('\n').next().unwrap_or("");
2921    let hashes = line.len() - line.trim_start_matches('#').len();
2922    if hashes == 0 {
2923        return span.start;
2924    }
2925    let after = &line[hashes..];
2926    span.start + hashes + (after.len() - after.trim_start_matches([' ', '\t']).len())
2927}
2928
2929struct Builder<'a> {
2930    nodes: &'a [FlatNode],
2931    /// The document source, consulted to place blank-line rows at the source
2932    /// offsets the caret should occupy on them (the AST drops blank lines).
2933    source: &'a str,
2934    /// The word-wrap column budget, or `None` to emit each block as a single
2935    /// unwrapped row (the frontend wraps).
2936    wrap: Option<usize>,
2937    rows: Vec<VRow>,
2938    /// Built alongside `rows`, never instead of them — see [`TableInfo`].
2939    tables: Vec<TableInfo>,
2940    /// The end offset of the last content emitted — the anchor for blank
2941    /// separator rows so the caret never snaps onto one.
2942    last_off: usize,
2943    /// The end of the last block the walk stepped over without drawing — a
2944    /// comment, which the rich view hides. `last_off` moves past it too, for the
2945    /// separators; this is kept apart so the trailing blank lines can be counted
2946    /// from it without also being counted from a code block's closing fence,
2947    /// which `last_off` likewise ends after. `0` until a hidden block is met.
2948    stepped_over: usize,
2949    /// How many rows each block image reserves, keyed by its destination — the
2950    /// frontend's per-image height, threaded in from [`crate::Doc::set_media_rows`]
2951    /// so [`Builder::block_media`] can size the placeholder without core doing any
2952    /// I/O. A destination absent from the map (or a `0`/`1` entry) reserves the
2953    /// bare one-row placeholder, which is the whole-document default and what
2954    /// every existing test — passing an empty map — still gets.
2955    surface: &'a Surface,
2956    /// The glyph a hard break renders as while the current inline run is built:
2957    /// a space in prose (a break folds into the flow the frontend wraps), but a
2958    /// newline (`\n`) inside a table cell, where a row is one source line and the
2959    /// only break it can carry is an explicit one that must show as a line of its
2960    /// own. Set around [`Builder::row_cells`] and otherwise left at `' '`.
2961    break_glyph: Cell<char>,
2962    /// Render a soft break (a bare newline inside a paragraph) as a line break
2963    /// where it was written, rather than folding it into the reflowed paragraph
2964    /// — the `LineFlow::Preserve` behaviour. A soft break emits a `'\n'` glyph
2965    /// (like a hard break in a cell), which [`Builder::emit_wrapped`] turns into
2966    /// a fresh visual row. `false` is the flowing-prose default. Inside a table
2967    /// cell (where `break_glyph` is already `'\n'`) it has no effect: a cell is
2968    /// one line and folds its own soft breaks regardless.
2969    preserve_soft: bool,
2970    /// The source byte range of the one line that should render its markup
2971    /// *raw* — the caret's line under `MarkupMode::Full` (see
2972    /// [`crate::Doc::reveal_line`]). `None` in every other mode and view, which
2973    /// is the delimiters-always-hidden behaviour every build had before the
2974    /// preference existed.
2975    ///
2976    /// Read only by [`Builder::revealed`], which every delimiter-bearing arm of
2977    /// [`Builder::inline`] consults. A range rather than a bare caret offset
2978    /// because the decision is per-*node*, not per-caret: a node is revealed
2979    /// when its span meets this line, so `*em*` shows both its asterisks even
2980    /// with the caret at one end of it.
2981    reveal: Option<Reveal>,
2982    /// The content ends of the hidden marks rendered since the last row was
2983    /// pushed — recorded as the inline walk meets each mark, and drained onto
2984    /// the rows as they are emitted (see [`Builder::take_mark_ends`]). A cell
2985    /// rather than a `&mut`, for the reason `break_glyph` is: the inline walk
2986    /// borrows the builder shared.
2987    pending_mark_ends: RefCell<Vec<usize>>,
2988    /// The inline atoms rendered since the last row was pushed, keyed by the
2989    /// formula's start offset — the offset the atom glyph carries, which is
2990    /// how [`Builder::take_math`] pairs each with its glyph once the wrap has
2991    /// decided which row it landed on. Drained the way `pending_mark_ends` is.
2992    pending_math: RefCell<Vec<(usize, MathMark)>>,
2993    /// Whether this walk met a formula at all, atom, block, or revealed — the
2994    /// fact [`BlockCache`] keeps per block so [`crate::Doc::reveal_line`] can
2995    /// tell whether the caret's line has anything to reveal without walking.
2996    saw_math: Cell<bool>,
2997    /// The presentation vocabulary in force at the block being walked — the
2998    /// keys the `div`s around it carry, folded together with the nearest
2999    /// winning, and [`Presentation::default`] at the top level.
3000    ///
3001    /// Saved and restored around each `div` in [`Builder::block`], so a block
3002    /// reads its own attributes over whatever its containers said and nothing
3003    /// leaks sideways to the block after it. It is per-*build* state rather
3004    /// than a parameter because every one of the dozen call sites of `block`
3005    /// would otherwise thread a value none of them care about.
3006    presentation: Presentation,
3007    /// The named families this walk has met, by the id its glyphs carry — see
3008    /// [`VisualMap::faces`]. A `RefCell` for [`pending_mark_ends`]'s reason:
3009    /// the inline walk borrows the builder shared, and a span's `data-font` is
3010    /// read from inside it.
3011    ///
3012    /// [`pending_mark_ends`]: Builder::pending_mark_ends
3013    faces: RefCell<FaceTable>,
3014}
3015
3016/// The six presentation keys as the walker carries them down a block tree —
3017/// the two that are the block's ([`Align`], [`LineHeight`]) and the three that
3018/// are a run's but may be written on the block ([`FontSize`], [`FaceRef`],
3019/// [`TextColor`]).
3020///
3021/// `Copy` and five `Option`s, because folding is the whole of what it does:
3022/// [`under`](Presentation::under) reads a container's attributes over an
3023/// existing set and a key the container does not name keeps the value it had.
3024/// That is the "nearest wins" rule stated once, rather than at each of the
3025/// three levels a key can be written at. A name and a value fold alike: the
3026/// nearer node wins whichever of the two forms either of them wrote.
3027#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3028struct Presentation {
3029    align: Option<Align>,
3030    line_height: Option<LineHeight>,
3031    size: Option<FontSize>,
3032    font: Option<FaceRef>,
3033    color: Option<TextColor>,
3034}
3035
3036impl Presentation {
3037    /// This set with whatever `attrs` names written over it — the nearer node's
3038    /// answer where it has one, the outer node's where it hasn't.
3039    ///
3040    /// `faces` is the build's intern table, which a named family is recorded in
3041    /// on the way past: the glyph carries the id and the table carries the
3042    /// string. Shared rather than `&mut` because the inline walk this feeds
3043    /// borrows the builder shared, the way `pending_mark_ends` does.
3044    fn under(self, attrs: &[(String, Option<String>)], faces: &RefCell<FaceTable>) -> Self {
3045        Self {
3046            align: Align::from_attrs(attrs).or(self.align),
3047            line_height: LineHeight::from_attrs(attrs).or(self.line_height),
3048            size: FontSize::from_attrs(attrs).or(self.size),
3049            font: faces.borrow_mut().face_from_attrs(attrs).or(self.font),
3050            color: TextColor::from_attrs(attrs).or(self.color),
3051        }
3052    }
3053
3054    /// `base` carrying the three run-level keys — the style a block's glyphs
3055    /// start from, which an attributed span inside it then writes over.
3056    fn over(self, base: Style) -> Style {
3057        base.size(self.size).font(self.font).color(self.color)
3058    }
3059}
3060
3061impl Builder<'_> {
3062    /// Note that the mark `id` closes with a hidden delimiter, so its content
3063    /// end is a caret home — unless the mark is empty, where the end is the
3064    /// start and there is nothing to extend.
3065    fn note_mark_end(&self, id: usize) {
3066        let node = &self.nodes[id];
3067        if let Some(content) = &node.content_span
3068            && content.end < node.span.end
3069            && !content.is_empty()
3070        {
3071            self.pending_mark_ends.borrow_mut().push(content.end);
3072        }
3073    }
3074
3075    /// The pending mark ends at or before `end_src`, for the row ending there
3076    /// — every mark rendered so far that closes on it. A mark's end never
3077    /// exceeds the end of the row its last glyph is on, so the leftovers are
3078    /// those of rows still to come.
3079    fn take_mark_ends(&self, end_src: usize) -> Vec<usize> {
3080        let mut pending = self.pending_mark_ends.borrow_mut();
3081        let (taken, kept): (Vec<usize>, Vec<usize>) =
3082            pending.drain(..).partition(|&o| o <= end_src);
3083        *pending = kept;
3084        taken
3085    }
3086
3087    /// The pending inline atoms whose glyph is on the row `glyphs` is about
3088    /// to become — each paired with the index of the [`Role::Math`] glyph
3089    /// carrying its offset, in glyph order. An atom whose glyph landed on an
3090    /// earlier row was taken then; one on a later row is left for it. The
3091    /// peer of [`take_mark_ends`](Self::take_mark_ends), and why an atom is
3092    /// keyed by offset: the wrap decides the row, and the offset is what the
3093    /// glyph still carries once it has.
3094    fn take_math(&self, glyphs: &[Glyph]) -> Vec<MathMark> {
3095        let mut pending = self.pending_math.borrow_mut();
3096        if pending.is_empty() {
3097            return Vec::new();
3098        }
3099        let mut out = Vec::new();
3100        for (i, g) in glyphs.iter().enumerate() {
3101            if g.style.role != Role::Math || !g.stop {
3102                continue;
3103            }
3104            if let Some(at) = pending.iter().position(|(src, _)| *src == g.src) {
3105                let (_, mut mark) = pending.remove(at);
3106                mark.glyph = Some(i);
3107                out.push(mark);
3108            }
3109        }
3110        out
3111    }
3112    /// Whether `span` belongs to the line that is showing its raw markup. True
3113    /// only when a reveal line is set *for markup* (`MarkupMode::Full`) and the
3114    /// two ranges actually meet — see [`Reveal::meets`] for what meeting is.
3115    fn revealed(&self, span: &Range<usize>) -> bool {
3116        self.reveal
3117            .as_ref()
3118            .is_some_and(|r| r.markup && r.meets(span))
3119    }
3120
3121    /// Whether a formula at `span` shows its TeX rather than its picture: it
3122    /// meets the reveal line, in *any* mode. A formula's content is its source
3123    /// and not its picture, so for it the choice is not between a clean
3124    /// surface and a raw one but between editable and not — which is why this
3125    /// does not read [`Reveal::markup`] the way [`revealed`](Self::revealed)
3126    /// does.
3127    fn math_revealed(&self, span: &Range<usize>) -> bool {
3128        self.reveal.as_ref().is_some_and(|r| r.meets(span))
3129    }
3130
3131    /// The `(opening, closing)` source byte ranges of a node's delimiters — the
3132    /// bytes its `span` holds that its `content_span` doesn't.
3133    ///
3134    /// This is how *every* inline delimiter is recovered, rather than a table of
3135    /// spellings per kind: twig gives `*em*` a span of `13..17` and a content
3136    /// span of `14..16`, so the gaps at each end are the delimiters, whatever
3137    /// they happen to be. That matters because one kind has many spellings —
3138    /// `*em*` and `_em_` are both emphasis, `` `x` `` and ``` ``x`` ``` both
3139    /// verbatim — and re-deriving the text from the source is the only way to
3140    /// show back what the author actually typed. It also gets a link's
3141    /// asymmetric `[` / `](dest)` right for free.
3142    ///
3143    /// `None` when the node has no content span, or when content and span
3144    /// coincide (nothing was elided, so there is nothing to reveal).
3145    fn delims(&self, id: usize) -> Option<(Range<usize>, Range<usize>)> {
3146        let node = &self.nodes[id];
3147        let content = node.content_span.clone()?;
3148        let span = node.span.clone();
3149        // A content span that escapes its own node's span means the two are
3150        // describing different things; reveal nothing rather than slice wildly.
3151        if content.start < span.start || content.end > span.end {
3152            return None;
3153        }
3154        let (open, close) = (span.start..content.start, content.end..span.end);
3155        // A delimiter that spans a newline isn't this line's to reveal — a setext
3156        // heading's `\n=====` underline is the case that arises in practice. It
3157        // would also inject a `'\n'` glyph, which `emit_wrapped` reads as a hard
3158        // row break, so the row would split where the author wrote no break.
3159        let multiline =
3160            |r: &Range<usize>| self.source.get(r.clone()).is_some_and(|s| s.contains('\n'));
3161        if multiline(&open) || multiline(&close) {
3162            return None;
3163        }
3164        (!open.is_empty() || !close.is_empty()).then_some((open, close))
3165    }
3166
3167    /// Emit the source bytes of `range` as revealed markup — real glyphs, each
3168    /// mapped to its own source byte and each a caret stop, so a delimiter shown
3169    /// is a delimiter that can be selected, edited and deleted like any other
3170    /// text. Styled [`Role::Delimiter`] on top of the run's own style, which is
3171    /// how a frontend tells scaffolding from prose and dims it.
3172    ///
3173    /// Deliberately *not* [`push_escaped_text`]: this is raw source, not parsed
3174    /// text, so there is no escape-driven drift between the two to correct.
3175    fn push_delim(&self, out: &mut Vec<Glyph>, range: &Range<usize>, base: Style) {
3176        let Some(text) = self.source.get(range.clone()) else {
3177            return;
3178        };
3179        push_text(out, text, range.start, base.role(Role::Delimiter));
3180    }
3181
3182    /// Render an inline node's children wrapped in its raw delimiters when the
3183    /// node is on the revealed line, and bare (delimiters resolved away) when it
3184    /// isn't — the shared body of every delimiter-bearing arm of
3185    /// [`inline`](Self::inline).
3186    ///
3187    /// `style` is the resolved styling the content still gets in *both* modes:
3188    /// revealing `*em*` shows the asterisks *and* keeps the text italic, the
3189    /// live-preview behaviour. Showing the markup is not the same as turning the
3190    /// rendering off — that is what [`crate::View::Source`] is for.
3191    fn inline_delimited(&self, id: usize, style: Style, out: &mut Vec<Glyph>) {
3192        let show = self
3193            .revealed(&self.nodes[id].span)
3194            .then(|| self.delims(id))
3195            .flatten();
3196        if let Some((open, _)) = &show {
3197            self.push_delim(out, open, style);
3198        }
3199        self.recurse(id, style, out);
3200        match &show {
3201            Some((_, close)) => self.push_delim(out, close, style),
3202            // Hidden, so the content's end has no glyph after it: give the
3203            // caret its home there.
3204            None => self.note_mark_end(id),
3205        }
3206    }
3207
3208    /// A verbatim span — or a formula drawn as its TeX — in the code style:
3209    /// its text at its content's offset, bracketed by its raw delimiters when
3210    /// `show` says the line is revealed and by nothing (plus the caret's home
3211    /// at the content's end) when it isn't.
3212    ///
3213    /// Not [`inline_delimited`](Self::inline_delimited): verbatim has no child
3214    /// nodes to recurse into — its content is its own `text` — so the fences
3215    /// bracket a [`push_text`] instead. The fences themselves keep
3216    /// `Role::Code`'s sibling treatment via [`push_delim`]'s role override.
3217    ///
3218    /// [`push_delim`]: Self::push_delim
3219    fn inline_verbatim(&self, id: usize, base: Style, out: &mut Vec<Glyph>, show: bool) {
3220        let node = &self.nodes[id];
3221        // The interior begins at `content_span.start` — past however many
3222        // backticks the fence used, which `span.start + 1` only guessed right
3223        // for a single one. Fall back to that guess if it's absent.
3224        let at = node
3225            .content_span
3226            .as_ref()
3227            .map_or(node.span.start + 1, |c| c.start);
3228        let style = base.role(Role::Code);
3229        let show = show.then(|| self.delims(id)).flatten();
3230        if let Some((open, _)) = &show {
3231            self.push_delim(out, open, style);
3232        }
3233        push_text(out, node.text.as_deref().unwrap_or(""), at, style);
3234        match &show {
3235            Some((_, close)) => self.push_delim(out, close, style),
3236            None => self.note_mark_end(id),
3237        }
3238    }
3239
3240    fn children(&self, id: usize) -> Vec<usize> {
3241        let mut out = Vec::new();
3242        let mut c = self.nodes[id].first_child;
3243        while let Some(cid) = c {
3244            out.push(cid.0 as usize);
3245            c = self.nodes[cid.0 as usize].next_sibling;
3246        }
3247        out
3248    }
3249
3250    /// Render a node's block children, a blank separator between each. `tight`
3251    /// suppresses the *fabricated* separator between adjacent children that share
3252    /// a source line boundary — a tight list item and the sub-list nested in it —
3253    /// while a real blank source line between them still opens a gap.
3254    fn blocks(&mut self, id: usize, pf: &[Glyph], pc: &[Glyph], tight: bool) {
3255        // Frontmatter (a leading `metadata` block) is document metadata, not
3256        // prose: hide it entirely in the rich-text view. Skipping it here means
3257        // no phantom blank rows for its lines and no separator before the first
3258        // real block — the document opens straight into its content.
3259        let kids: Vec<usize> = self
3260            .children(id)
3261            .into_iter()
3262            .filter(|&c| self.nodes[c].kind != Kind::Metadata)
3263            .collect();
3264        let mut above: Option<BlockClass> = None;
3265        for child in kids {
3266            let below = BlockClass::from_node_kind(&self.nodes[child].kind);
3267            let before_sep = self.rows.len();
3268            if let Some(above) = above {
3269                self.emit_separators_before(
3270                    self.nodes[child].span.start,
3271                    pc,
3272                    !tight,
3273                    Boundary { above, below },
3274                );
3275            }
3276            // The first *drawn* child wears the first-row prefix (a bullet, a
3277            // footnote label), not the first child: a comment opening a list
3278            // item draws nothing, and the bullet belongs to what follows it.
3279            let first = if above.is_none() { pf } else { pc };
3280            if self.block_or_hidden(child, before_sep, first, pc) {
3281                above = Some(below);
3282            }
3283        }
3284    }
3285
3286    /// Render `child` after the separator [`Builder::emit_separators_before`]
3287    /// spelled for it from row `before_sep` on, and say whether it drew
3288    /// anything.
3289    ///
3290    /// A block that draws no rows — an HTML comment, which the rich view hides
3291    /// the way it hides frontmatter — is still *there* in the source, and the
3292    /// walk has to step over it: `last_off` moves past it so the next separator
3293    /// counts the blank lines from its end, not from wherever the last drawn
3294    /// block stopped. Left where it was, the separator counted every line of the
3295    /// comment as a blank row; and the cached path, whose per-block builder
3296    /// starts at offset 0, handed back a `last_off` of 0 and counted every line
3297    /// of the *document* — one phantom blank row per source line, once per
3298    /// comment. The separator drawn for it is taken back too, so a hidden block
3299    /// leaves no gap of its own: what stands either side of it meets across one
3300    /// boundary, as if the comment were not there.
3301    fn block_or_hidden(
3302        &mut self,
3303        child: usize,
3304        before_sep: usize,
3305        pf: &[Glyph],
3306        pc: &[Glyph],
3307    ) -> bool {
3308        let after_sep = self.rows.len();
3309        self.block(child, pf, pc);
3310        if self.rows.len() > after_sep {
3311            return true;
3312        }
3313        self.rows.truncate(before_sep);
3314        let end = self.nodes[child].span.end;
3315        self.last_off = self.last_off.max(end);
3316        self.stepped_over = self.stepped_over.max(end);
3317        false
3318    }
3319
3320    /// Render an explicit, ordered list of top-level blocks — [`Builder::blocks`]
3321    /// for a walk that isn't "the children of one node". The document's top level
3322    /// no longer is: a footnote definition is a root beside `doc`, not under it,
3323    /// and [`top_level`] merges it into this list by source position.
3324    ///
3325    /// The separator between blocks is spelled by the same
3326    /// [`Builder::emit_separators_before`] the incremental top-level walk in
3327    /// [`build_cached`] uses, so the two paths can't drift on how a boundary
3328    /// looks.
3329    ///
3330    /// Returns the class of the last block that drew anything — what the
3331    /// trailing blank lines close — or `None` when nothing did.
3332    fn top_blocks(&mut self, ids: &[usize], hidden_end: usize) -> Option<BlockClass> {
3333        let mut above: Option<BlockClass> = None;
3334        for &child in ids {
3335            let below = BlockClass::from_node_kind(&self.nodes[child].kind);
3336            let before_sep = self.rows.len();
3337            if let Some(above) = above {
3338                self.emit_separators_before(
3339                    self.nodes[child].span.start,
3340                    &[],
3341                    true,
3342                    Boundary { above, below },
3343                );
3344            } else {
3345                let from = self.leading_from(hidden_end);
3346                self.emit_leading_blank_lines(from, self.nodes[child].span.start, below);
3347            }
3348            if self.block_or_hidden(child, before_sep, &[], &[]) {
3349                above = Some(below);
3350            }
3351        }
3352        above
3353    }
3354
3355    /// Emit the blank separator row(s) that sit between a block ending at the
3356    /// current `last_off` and the next block starting at `next_start`, wearing
3357    /// the continuation prefix `pc`. Shared by [`Builder::blocks`] and the
3358    /// incremental top-level walk so the two can't drift on how a boundary is
3359    /// spelled.
3360    ///
3361    /// The blank line(s) between two blocks are real caret stops, each needing
3362    /// its *own* source offset — one strictly past the previous block's content,
3363    /// else it collides with that block's last row and `pos_of_offset`
3364    /// (first-match-wins) would resolve the caret onto the wrong row, pinning
3365    /// downward motion there.
3366    ///
3367    /// One row *per* blank source line, not a single collapsed separator: an
3368    /// empty paragraph opened between two blocks (Enter in the gap,
3369    /// `…\n\n\n\n…`) must be a navigable empty row, not vanish — else the caret
3370    /// in it snaps onto the *next* block's start and Enter looks like it did
3371    /// nothing.
3372    fn emit_separators_before(
3373        &mut self,
3374        next_start: usize,
3375        pc: &[Glyph],
3376        synthetic: bool,
3377        boundary: Boundary,
3378    ) {
3379        let mut offs = self.blank_rows_between(self.last_off, next_start);
3380        // Under a `</div>` the first blank line is the one Markdown needs to
3381        // end the div, not a line the author opened. Preserve flow drew it as
3382        // one, and Backspace there took it and glued the block below onto the
3383        // closing tag, as raw HTML.
3384        if self.preserve_soft && offs.first().is_some_and(|&o| self.closes_div_above(o)) {
3385            offs.remove(0);
3386            if offs.is_empty() {
3387                return;
3388            }
3389        }
3390        if offs.is_empty() {
3391            if !synthetic {
3392                // A tight list item's own text sits directly above the sub-list
3393                // nested in it — no fabricated gap. The "breathe" row belongs
3394                // between free-standing blocks, not between an item and its
3395                // child list, which the source writes on the very next line. A
3396                // real blank source line (a loose list) still lands a gap below,
3397                // because `blank_rows_between` found it and we never reach here.
3398                return;
3399            }
3400            // A tight gap with no blank line (e.g. a heading directly above its
3401            // text): keep the one conventional separator row so blocks still
3402            // breathe, as they always have.
3403            offs.push(self.blank_line_offset(self.last_off, next_start));
3404        }
3405        let last = offs.len() - 1;
3406        for (k, end_src) in offs.into_iter().enumerate() {
3407            // Only the drawn-only rows carry the boundary: the navigable blank
3408            // lines between them (and every blank line under preserve-soft flow)
3409            // are somewhere text can go, not a gap between blocks, and a frontend
3410            // that shrank one would be shrinking a line the author is typing on.
3411            let drawn = !self.preserve_soft && (k == 0 || k == last);
3412            // The blank line a boundary is *drawn* with isn't a place text can
3413            // go. The first one closes the block above and the last one opens the
3414            // block below — with a single blank line, the usual case, doing both
3415            // at once. Typing on either just continues the paragraph it abuts,
3416            // since the blank line it would need to be a paragraph of its own is
3417            // the very line being typed on. So they're a gap, like a table's
3418            // border: drawn, clickable, never a caret's home.
3419            //
3420            // The lines *between* them are the real ones. That's what Enter
3421            // opens: it inserts a paragraph break (`\n\n`), which leaves a blank
3422            // line spare on each side and the caret on the navigable line
3423            // between them.
3424            //
3425            // Preserve flow is the exception: there a bare `\n` is a visible line
3426            // break the author edits directly, so a lone blank line *is* a caret
3427            // home — typing on it makes the soft break the mode exists to show,
3428            // and Enter at a line's end lands the caret on exactly this row. So no
3429            // separator is drawn-only; every blank line is navigable.
3430            self.rows.push(VRow {
3431                glyphs: pc.to_vec(),
3432                end_src,
3433                decoration: drawn,
3434                code: false,
3435                code_lang: None,
3436                directive: false,
3437                directive_label: None,
3438                media: None,
3439                task: None,
3440                leaf_directive: None,
3441                heading: None,
3442                // A line the author can type on is a line of the block
3443                // around it, and keeps its spacing and alignment.
3444                align: (!drawn).then_some(self.presentation.align).flatten(),
3445                line_height: (!drawn).then_some(self.presentation.line_height).flatten(),
3446                boundary: drawn.then_some(boundary),
3447                mark_ends: Vec::new(),
3448                math: Vec::new(),
3449            });
3450        }
3451    }
3452
3453    /// The blank lines above the first block that draws anything, from `from`
3454    /// — the first line past any hidden frontmatter or comment — to the line
3455    /// holding `next_start`. [`Builder::emit_trailing_blank_lines`] turned
3456    /// upside down: nothing above needs a gap, so every line is an empty
3457    /// paragraph but the last, which is the gap that opens the block below.
3458    ///
3459    /// One blank line is only that gap, and draws nothing, which keeps the
3460    /// usual line under frontmatter out of sight. Two are the empty paragraph
3461    /// Enter opens at the first block's start (`\n\nHello`), which drew
3462    /// nothing either: the caret stayed with the text and the text stayed put.
3463    /// Preserve flow draws every line, as it does between blocks, but the one
3464    /// under frontmatter, which is the frontmatter's and not the author's.
3465    fn emit_leading_blank_lines(&mut self, from: usize, next_start: usize, below: BlockClass) {
3466        let next_line_start = self.source[..next_start].rfind('\n').map_or(0, |p| p + 1);
3467        let mut offs = Vec::new();
3468        let mut start = from;
3469        while start < next_line_start {
3470            offs.push(start);
3471            match self.source[start..next_line_start].find('\n') {
3472                Some(k) => start += k + 1,
3473                None => break,
3474            }
3475        }
3476        if self.preserve_soft {
3477            if from > 0 && !offs.is_empty() {
3478                offs.remove(0);
3479            }
3480        } else if offs.len() < 2 {
3481            return;
3482        }
3483        let last = offs.len().saturating_sub(1);
3484        for (k, end_src) in offs.into_iter().enumerate() {
3485            let drawn = !self.preserve_soft && k == last;
3486            self.rows.push(VRow {
3487                glyphs: Vec::new(),
3488                end_src,
3489                decoration: drawn,
3490                code: false,
3491                code_lang: None,
3492                directive: false,
3493                directive_label: None,
3494                media: None,
3495                task: None,
3496                leaf_directive: None,
3497                heading: None,
3498                align: None,
3499                line_height: None,
3500                boundary: drawn.then_some(Boundary {
3501                    above: BlockClass::Paragraph,
3502                    below,
3503                }),
3504                mark_ends: Vec::new(),
3505                math: Vec::new(),
3506            });
3507        }
3508    }
3509
3510    /// Whether the line above the one starting at `line` is a `</div>`.
3511    fn closes_div_above(&self, line: usize) -> bool {
3512        let Some(above) = self.source[..line].strip_suffix('\n') else {
3513            return false;
3514        };
3515        let above = above.strip_suffix('\r').unwrap_or(above);
3516        let start = above.rfind('\n').map_or(0, |p| p + 1);
3517        above[start..].trim() == "</div>"
3518    }
3519
3520    /// The blank lines between a `<div>`'s last block and its closing tag,
3521    /// drawn inside the div, as the lines between two of its blocks are.
3522    ///
3523    /// One blank line is the one Markdown needs before `</div>`, and two are
3524    /// that and the gap, so neither draws (in Preserve flow, which has no
3525    /// gap, the second does). A third is the empty paragraph
3526    /// Enter opens at the end of the div's last block — the end of a line
3527    /// with a line height or an alignment on it — which drew nothing: the
3528    /// key looked dead, and each press left another line nobody could see.
3529    fn emit_div_trailing_lines(&mut self, id: usize, pc: &[Glyph]) {
3530        let Some(&last) = self.children(id).last() else {
3531            return;
3532        };
3533        if self.rows.is_empty() {
3534            return;
3535        }
3536        let end = self.nodes[id].span.end.min(self.source.len());
3537        let from = block_line(self.source, self.last_off).1;
3538        if from >= end {
3539            return;
3540        }
3541        let Some(close) = self.source[from..end].rfind("</div>") else {
3542            return;
3543        };
3544        let close_line = self.source[..from + close].rfind('\n').map_or(0, |p| p + 1);
3545        let mut offs = Vec::new();
3546        let mut start = from;
3547        while start < close_line {
3548            if !self.source[start..close_line]
3549                .lines()
3550                .next()
3551                .unwrap_or("")
3552                .trim()
3553                .is_empty()
3554            {
3555                return;
3556            }
3557            offs.push(start);
3558            match self.source[start..close_line].find('\n') {
3559                Some(k) => start += k + 1,
3560                None => break,
3561            }
3562        }
3563        // Preserve flow draws every blank line but the one `</div>` needs.
3564        if offs.len() < if self.preserve_soft { 2 } else { 3 } {
3565            return;
3566        }
3567        offs.pop();
3568        let above = BlockClass::from_node_kind(&self.nodes[last].kind);
3569        for (k, end_src) in offs.into_iter().enumerate() {
3570            let drawn = !self.preserve_soft && k == 0;
3571            self.rows.push(VRow {
3572                glyphs: pc.to_vec(),
3573                end_src,
3574                decoration: drawn,
3575                code: false,
3576                code_lang: None,
3577                directive: false,
3578                directive_label: None,
3579                media: None,
3580                task: None,
3581                leaf_directive: None,
3582                heading: None,
3583                align: (!drawn).then_some(self.presentation.align).flatten(),
3584                line_height: (!drawn).then_some(self.presentation.line_height).flatten(),
3585                boundary: drawn.then_some(Boundary {
3586                    above,
3587                    below: BlockClass::Paragraph,
3588                }),
3589                mark_ends: Vec::new(),
3590                math: Vec::new(),
3591            });
3592        }
3593    }
3594
3595    /// Where the lines above the first drawn block begin: past the hidden
3596    /// frontmatter, or past the line of the last hidden block the walk
3597    /// stepped over, whichever is later.
3598    fn leading_from(&self, hidden_end: usize) -> usize {
3599        if self.last_off == 0 {
3600            return hidden_end;
3601        }
3602        block_line(self.source, self.last_off).1.max(hidden_end)
3603    }
3604
3605    /// One block, drawn under whatever presentation the containers around it
3606    /// impose.
3607    ///
3608    /// A container named `div` with `Element` origin is transparent already —
3609    /// its children draw as themselves — and it now also *contributes* its
3610    /// vocabulary keys to every block it holds. That is the reading side of
3611    /// twig's own rule for where a Markdown block's attributes live: there is
3612    /// no attribute syntax to put on the paragraph, so `set_block_attrs` writes
3613    /// a `<div …>` around it, and reading one back has to look through the div.
3614    /// `<div class="center">` around three paragraphs centres all three, which
3615    /// is what the author of that HTML meant, and around one is the sole-child
3616    /// shape twig writes.
3617    ///
3618    /// Saved and restored rather than pushed onto a stack, so a nested div
3619    /// reads its own keys over its parent's and the block *after* the div is
3620    /// unaffected.
3621    fn block(&mut self, id: usize, pf: &[Glyph], pc: &[Glyph]) {
3622        if element_tag(&self.nodes[id]) == Some("div") {
3623            let saved = self.presentation;
3624            self.presentation = saved.under(&self.nodes[id].attrs, &self.faces);
3625            self.block_kind(id, pf, pc);
3626            self.emit_div_trailing_lines(id, pc);
3627            self.presentation = saved;
3628            // Step the walk past the closing `</div>`, as the fenced-div arm
3629            // below anchors past its `:::`. The tag sits on a line of its own
3630            // after the last child and the blank line under it, and the rich
3631            // view draws nothing for it — so left where the last child ended,
3632            // the separator logic read the tag's line as a blank line between
3633            // the div and the block below, and drew a navigable empty row there
3634            // that the author never opened and Backspace could not close; and
3635            // at the end of the document the trailing count read it as an empty
3636            // paragraph the author had left. It is hidden markup the walk steps
3637            // over, which is what `stepped_over` records, so both counts start
3638            // past it.
3639            let end = self.nodes[id].span.end;
3640            self.last_off = self.last_off.max(end);
3641            self.stepped_over = self.stepped_over.max(end);
3642            return;
3643        }
3644        self.block_kind(id, pf, pc);
3645    }
3646
3647    fn block_kind(&mut self, id: usize, pf: &[Glyph], pc: &[Glyph]) {
3648        let node = &self.nodes[id];
3649        match node.kind.as_str() {
3650            "doc" | "section" => self.blocks(id, pf, pc, false),
3651            "heading" => {
3652                // A heading whose only visible content is a single image — a
3653                // banner set in an `<h1>` (`<h1><picture><img></picture></h1>`),
3654                // or `# ![](banner.png)` — is a block picture, not text. Render
3655                // it as one; anything with real heading text falls through.
3656                if let Some((m, kind)) = self.media_only(id) {
3657                    self.block_media(m, kind, id, pf);
3658                    return;
3659                }
3660                let level = node.level.unwrap_or(1);
3661                // A `data-size` on a heading scales the *heading's* ramp, not
3662                // the body's — the role and the step compose rather than
3663                // compete, which is the same thing a colour does to a link.
3664                let pres = self.presentation.under(&node.attrs, &self.faces);
3665                let style = pres.over(heading_style(level));
3666                let mut glyphs = Vec::new();
3667                // On the revealed line the `# ` comes back as real, editable
3668                // text in front of the heading. Only the opening marker: a
3669                // closing `#`-run (`## title ##`) is covered by the same
3670                // `delims` pair, and a setext underline is excluded there for
3671                // being on another line entirely.
3672                if let Some((open, close)) =
3673                    self.revealed(&node.span).then(|| self.delims(id)).flatten()
3674                {
3675                    self.push_delim(&mut glyphs, &open, style);
3676                    glyphs.extend(self.inline_children_with_trailing(id, style));
3677                    self.push_delim(&mut glyphs, &close, style);
3678                } else {
3679                    glyphs = self.inline_children_with_trailing(id, style);
3680                }
3681                // An *empty* heading — `# ` with nothing typed after it, which is
3682                // what the toolbar's H1 leaves on a blank line — has no glyph for
3683                // its row to end on, so the fallback below is the row's whole
3684                // extent: its only caret stop, and the offset every row after it
3685                // is measured from. The block's start is the wrong answer for
3686                // both, because it sits *in front of* the `# ` the rich view
3687                // hides: the caret drew (and typed) before the hashes, and the
3688                // rows below inherited an offset short by the marker's length,
3689                // which put the caret on one of them the moment the heading grew
3690                // text. Its content's start is where the caret belongs.
3691                let home = heading_content_start(self.source, &node.span);
3692                let first = self.rows.len();
3693                self.emit_wrapped(glyphs, home, pf, pc);
3694                // Stamp the level on every row the heading just emitted — a
3695                // wrapped heading's continuation rows as much as its first, and
3696                // an empty one's single glyphless row, which is the whole point
3697                // (see [`VRow::heading`]).
3698                for row in &mut self.rows[first..] {
3699                    row.heading = Some(level.min(255) as u8);
3700                    row.align = pres.align;
3701                    row.line_height = pres.line_height;
3702                }
3703            }
3704            "block_quote" => {
3705                let (start, end) = (node.span.start, node.span.end);
3706                let gutter = synth("│ ", Role::QuoteGutter, start);
3707                let f = concat(pf, &gutter);
3708                let c = concat(pc, &gutter);
3709                // A childless quote — a bare `> ` on an otherwise blank line,
3710                // which is what the toolbar's Quote button leaves there — has no
3711                // inner block to carry the gutter or a caret home, so `blocks`
3712                // emitted *nothing at all*: the quote didn't merely draw
3713                // unstyled, it disappeared, and a document that was only `> `
3714                // rendered zero rows with the caret nowhere to stand. Emit the
3715                // gutter row itself, ending just past the marker, exactly as an
3716                // empty `list_item` emits its bare bullet.
3717                if self.children(id).is_empty() {
3718                    self.push_row_at(f, end.min(self.source.len()));
3719                } else {
3720                    self.blocks(id, &f, &c, false);
3721                    self.emit_quote_trailing_lines(&c, end);
3722                }
3723            }
3724            // A generic `:::name{.class}` fenced-div container (twig's
3725            // `directive`, container form). Core is agnostic of `name` — it's
3726            // the host app's vocabulary (diaryx's `vis` for audience
3727            // visibility, say) and isn't available here regardless: twig only
3728            // threads an `element`'s tag name through `FlatNode::name`, not a
3729            // directive's own identifier. Every row gets marked `directive` (a
3730            // frontend draws a tinted panel around each maximal run, the
3731            // `code`/`code_block` recipe) and the first row carries a label —
3732            // the way a code fence's language rides only its first row.
3733            //
3734            // The label reads BOTH attribute conventions diaryx content
3735            // actually uses: twig's own dot-prefixed classes (`{.public
3736            // .family}`, one combined `class` attr) and bare pandoc-style
3737            // words with no leading dot (`{public family}` — the syntax
3738            // `diaryx_core::visibility`'s hand-rolled publish-time filter and
3739            // apps/web's directive serializer both write; twig parses each
3740            // bare word as its own attribute with an empty value, per
3741            // `languages/markdown/attributes.zig`). Reading only `.class`
3742            // would leave every *existing* diaryx `:::vis{...}` block
3743            // unlabeled.
3744            // Only the *container* form is the panel below. A `text` directive
3745            // is inline and never reaches the block walker (see `is_inline`); a
3746            // `leaf` one is a standalone block with no body, drawn as a
3747            // placeholder the way an image is.
3748            "container"
3749                if container_is_directive(node)
3750                    && node.directive_form == Some(DirectiveForm::Leaf) =>
3751            {
3752                self.block_directive(id, pf);
3753            }
3754            // HTML and AsciiDoc spell `insert_directive`'s page break in ways
3755            // of their own — `<page-break></page-break>`, an *element* with
3756            // no form at all, and `<<<`, a directive with none — so neither
3757            // meets the arm above. Both are named `page-break` and empty, and
3758            // both draw the same placeholder, for the same reason djot's
3759            // fence below does: a frontend that paginates on the mark must
3760            // not be able to tell which format the file is in. Narrow to the
3761            // one name: an arbitrary empty custom element is not a leaf
3762            // directive.
3763            "container"
3764                if node.name.as_deref() == Some(crate::doc::PAGE_BREAK)
3765                    && node.directive_form.is_none()
3766                    && node.origin.is_some()
3767                    && self.children(id).is_empty() =>
3768            {
3769                self.block_directive(id, pf);
3770            }
3771            // djot has no *leaf* directive form. `insert_directive` spells the
3772            // same document as an empty `::: page-break` fence — a container
3773            // with nothing in it — and the name comes back as the fence's one
3774            // class rather than as the node's name, because djot's div is
3775            // anonymous. Draw it as the placeholder Markdown's `::page-break`
3776            // gets, so a frontend that paginates on a `page-break`
3777            // [`DirectiveMark`] cannot tell which format the file is in.
3778            //
3779            // Narrow on purpose: only an *anonymous* empty fence. A Markdown
3780            // `:::note` with nothing in it keeps the reading it has, because
3781            // its name is its own and nothing about it says "a block with no
3782            // body" the way djot's spelling of a leaf directive does.
3783            "container"
3784                if container_is_directive(node)
3785                    && node.directive_form == Some(DirectiveForm::Container)
3786                    && node.name.as_deref().unwrap_or_default().is_empty()
3787                    && self.children(id).is_empty()
3788                    && !leaf_directive_identity(node).0.is_empty() =>
3789            {
3790                self.block_directive(id, pf);
3791            }
3792            "container" if container_is_directive(node) => {
3793                let label = directive_attr_label(&node.attrs);
3794                let start_row = self.rows.len();
3795                self.blocks(id, pf, pc, false);
3796                for (i, row) in self.rows[start_row..].iter_mut().enumerate() {
3797                    row.directive = true;
3798                    if i == 0 {
3799                        row.directive_label = label.clone();
3800                    }
3801                }
3802                // Anchor the block's end past its closing `:::` fence, exactly as
3803                // the code-block arm anchors past its ```` ``` ````. A container's
3804                // last content row ends at its last *child*, before the fence and
3805                // the blank line under it, so the separator logic counted the
3806                // fence line as a blank row of its own and drew a second boundary
3807                // — one gap's worth of margin twice, under every fenced div.
3808                self.last_off = node.span.end;
3809            }
3810            "bullet_list" | "ordered_list" | "task_list" => {
3811                let ordered = node.kind == Kind::OrderedList;
3812                let mut item_no = 0usize;
3813                let kids = self.children(id);
3814                for (i, child) in kids.iter().copied().enumerate() {
3815                    let kind = &self.nodes[child].kind;
3816                    if *kind == Kind::ListItem || *kind == Kind::TaskListItem {
3817                        let start = self.nodes[child].span.start;
3818                        item_no += 1;
3819                        // A task item's box replaces the bullet rather than
3820                        // joining it. The `[ ] ` that spells it is markup twig
3821                        // has already consumed — the item's paragraph *content*
3822                        // starts past it — so without a drawn box a task item
3823                        // was indistinguishable from a plain bullet, ticked or
3824                        // not. `☐`/`☑` is the marker for the same reason `•` is:
3825                        // it stands where the source's own marker stands. Which
3826                        // way it faces is `checked`, straight off the node.
3827                        let checked = self.nodes[child].checked;
3828                        let marker = match (checked, ordered) {
3829                            (Some(true), _) => "☑ ".to_string(),
3830                            (Some(false), _) => "☐ ".to_string(),
3831                            (None, true) => format!("{item_no}. "),
3832                            (None, false) => "• ".to_string(),
3833                        };
3834                        let bullet = synth(&marker, Role::ListMarker, start);
3835                        // The item's later rows wear the marker's own
3836                        // characters, drawn blank: the width a proportional
3837                        // face gives the marker, whatever the marker is.
3838                        let indent = synth(&marker, Role::ListIndent, start);
3839                        let first_row = self.rows.len();
3840                        self.block(child, &concat(pc, &bullet), &concat(pc, &indent));
3841                        // On the item's first row, the way `code_lang` rides the
3842                        // first row of its block.
3843                        if let (Some(c), Some(row)) = (checked, self.rows.get_mut(first_row)) {
3844                            row.task = Some(c);
3845                        }
3846                    } else {
3847                        // twig can nest a *following* top-level block as a direct
3848                        // child of the list rather than a sibling of it — e.g.
3849                        // `- item\n\n> quote` parses the block quote under the
3850                        // `bullet_list`. It isn't a list item, so render it de-nested:
3851                        // no bullet, at the list's own prefix, with the usual block
3852                        // separator — never `• │ quote`.
3853                        if i > 0 {
3854                            self.emit_separators_before(
3855                                self.nodes[child].span.start,
3856                                pc,
3857                                true,
3858                                Boundary {
3859                                    above: BlockClass::from_node_kind(
3860                                        &self.nodes[kids[i - 1]].kind,
3861                                    ),
3862                                    below: BlockClass::from_node_kind(&self.nodes[child].kind),
3863                                },
3864                            );
3865                        }
3866                        self.block(child, pc, pc);
3867                    }
3868                }
3869            }
3870            "list_item" | "task_list_item" => {
3871                // A childless item — the empty bullet you get the instant you
3872                // press Enter to open a new one — has no inner block to carry the
3873                // marker prefix or a caret home, so `blocks` would emit nothing
3874                // and the new bullet simply wouldn't appear until something was
3875                // typed into it. Emit the prefixed row itself, ending at a caret
3876                // stop just past the marker (the item's `span.end`), the way an
3877                // empty paragraph emits its one prefixed row via `emit_wrapped`.
3878                if self.children(id).is_empty() {
3879                    let home = self.nodes[id].span.end.min(self.source.len());
3880                    self.push_row_at(pf.to_vec(), home);
3881                } else {
3882                    // Tight: an item's text and the list nested under it butt
3883                    // together (`• a` / `  • b`), no fabricated blank row between —
3884                    // a loose item's real blank line still parts them.
3885                    self.blocks(id, pf, pc, true);
3886                }
3887            }
3888            // A footnote *definition* (`[^1]: the note`). It reaches this walker
3889            // only because [`top_level`] merges it back in — twig hangs it off no
3890            // parent at all, so a walk from `doc` never sees one and every byte
3891            // of its body used to render as nothing.
3892            //
3893            // Drawn as a hanging-indent item, the way a list item is: the marker
3894            // reads `[1] `, matching the `[1]` its references render as, so the
3895            // two can be paired by eye, and the body wraps under it. The marker
3896            // is synthetic decoration (one shared offset, never a caret stop) —
3897            // the `[^1]: ` that spells it in the source is markup, hidden like a
3898            // heading's `# `.
3899            "footnote" => {
3900                let (start, end) = (node.span.start, node.span.end);
3901                let source = self.source;
3902                let marker = format!("[{}] ", footnote_label(source, start).unwrap_or(""));
3903                let f = concat(pf, &synth(&marker, Role::ListMarker, start));
3904                let c = concat(pc, &synth(&marker, Role::ListIndent, start));
3905                if self.children(id).is_empty() {
3906                    // A definition with no body yet — the instant `[^1]: ` has
3907                    // been typed and nothing after it. `blocks` would emit
3908                    // nothing and the definition simply wouldn't appear, so emit
3909                    // the marker row itself with a caret home just past it,
3910                    // exactly as an empty list item does.
3911                    self.push_row_at(f, end.min(source.len()));
3912                } else {
3913                    self.blocks(id, &f, &c, false);
3914                }
3915            }
3916            // A link reference definition (`[foo]: /url`): resolved by label
3917            // into the links that use it, and drawn nowhere — the rich view has
3918            // no more use for its line than for a comment's. It is walked at all
3919            // (see [`top_level`]) so [`Builder::block_or_hidden`] can step the
3920            // walk past its bytes rather than count them as blank lines.
3921            "reference" => {}
3922            "table" => self.table(id, pf, pc),
3923            "code_block" => {
3924                let style = Style::default().role(Role::Code);
3925                let text = node.text.clone().unwrap_or_default();
3926                // Cut the block's *terminator*, not every trailing newline. A
3927                // block whose last line is empty spells that as a second `\n`,
3928                // and `trim_end_matches` ate it along with the terminator: the
3929                // Return that made the line got no row, so the caret placed on
3930                // it fell through to the paragraph below and typing landed
3931                // outside the block. twig's `content_span` is `text` less
3932                // exactly this one newline, so cutting one and no more is also
3933                // what keeps `code_line_offsets` lined up.
3934                let lines: Vec<&str> = text
3935                    .strip_suffix('\n')
3936                    .unwrap_or(text.as_str())
3937                    .split('\n')
3938                    .collect();
3939                // Each line at its own source offset, so the caret can walk the
3940                // code a character at a time like any other text. Where the
3941                // lines can't be lined up with the source there's no honest
3942                // offset to give, so the block maps coarsely to its start (and
3943                // stays a source-view job, as all of it once was).
3944                let offs = node
3945                    .content_span
3946                    .as_ref()
3947                    .and_then(|c| self.code_line_offsets(c, &lines));
3948                // The fence's info string, carried on the block's first row as
3949                // its language label (`None` for an indented block or a bare
3950                // fence). Kept on the row so it rides the block cache.
3951                let lang = code_language(self.source, node.span.start);
3952                // The block's syntax highlighting, a token per byte range of
3953                // each line — `None` unless the fence names a language the
3954                // grammars know (and unless the `syntax` feature is on). Done
3955                // here, once per build of the block, because the rows it
3956                // colours ride the block cache: an edit elsewhere in the
3957                // document reuses them, tokens and all.
3958                let tokens = lang.as_deref().and_then(|l| code_tokens(l, &lines));
3959                for (i, raw) in lines.iter().enumerate() {
3960                    let at = offs.as_ref().map_or(node.span.start, |o| o[i]);
3961                    // No gutter glyph: the block is set apart by the border and
3962                    // tint a frontend draws around the whole run of `code` rows,
3963                    // not by a per-line mark. Just the block prefix and the
3964                    // code text: the first-row prefix on the first line (a list
3965                    // item's marker) and the continuation on the rest (its
3966                    // indent), as a wrapped paragraph takes them.
3967                    let mut glyphs: Vec<Glyph> = if i == 0 { pf } else { pc }.to_vec();
3968                    match tokens.as_ref().and_then(|t| t.get(i)) {
3969                        Some(spans) => push_code_text(&mut glyphs, raw, at, style, spans),
3970                        None => push_text(&mut glyphs, raw, at, style),
3971                    }
3972                    // Explicitly past the line's *text*: a blank code line has no
3973                    // glyph, and any prefix's offset would put the row's end
3974                    // inside the next line.
3975                    self.push_row_at(glyphs, at + raw.len());
3976                    if let Some(row) = self.rows.last_mut() {
3977                        row.code = true;
3978                        if i == 0 {
3979                            row.code_lang = lang.clone();
3980                        }
3981                    }
3982                }
3983                // Anchor the block's end past its closing fence. Its last content
3984                // row ends at the last code line, before the ``` and the blank
3985                // line under it; without this the separator logic would count the
3986                // closing-fence line as its own blank row and open a phantom
3987                // second gap below the block.
3988                self.last_off = node.span.end;
3989            }
3990            "thematic_break" => {
3991                let full = self.wrap.unwrap_or(UNWRAPPED_RULE_WIDTH);
3992                let w = full.saturating_sub(prefix_width(pf)).max(4);
3993                let mut glyphs = pf.to_vec();
3994                for _ in 0..w {
3995                    glyphs.push(Glyph {
3996                        ch: '─',
3997                        style: Style::default().role(Role::Rule),
3998                        src: node.span.start,
3999                        // A rule is a block the caret can sit on, as it always
4000                        // has; it maps coarsely to the block's start.
4001                        stop: true,
4002                    });
4003                }
4004                // The dashes share one caret home in front of the atomic block,
4005                // while the row's end is the second home, past the newline that
4006                // ends the rule's line. Without that trailing stop a final rule
4007                // made the document end unreachable: Right could not cross it
4008                // and a click in the empty space below it snapped back before
4009                // the rule.
4010                let (text_end, after_line) = block_line(self.source, node.span.end);
4011                self.push_row_at(glyphs, after_line);
4012                // The walk stands at the rule itself, though, as it stands at
4013                // the end of any other block's last line: the lines under a
4014                // rule are counted from the newline that ends it. Counted from
4015                // the home past that newline, every gap under a rule came up a
4016                // line short, and the empty paragraph Enter opens beneath one
4017                // drew as nothing at all.
4018                self.last_off = text_end;
4019            }
4020            // A block-level image node with no wrapping paragraph — a promoted
4021            // top-level HTML `<img>` lands as a direct `doc` child like this
4022            // (a Markdown `![](…)` comes wrapped in a `para`, handled below).
4023            "image" => self.block_media(id, MediaKind::Image, id, pf),
4024            // The same case for a promoted top-level `<video>`/`<audio>`, which
4025            // arrives as a generic `container` rather than a node kind of its
4026            // own. It can't be found by the `media_only` scan below the way a
4027            // wrapped one is: that scan looks at a wrapper's *children*, and here
4028            // the media element is itself the block.
4029            "container" if matches!(element_tag(node), Some("video") | Some("audio")) => {
4030                let kind = match element_tag(node) {
4031                    Some("audio") => MediaKind::Audio,
4032                    _ => MediaKind::Video,
4033                };
4034                self.block_media(id, kind, id, pf);
4035            }
4036            _ => {
4037                // A container of blocks, or an inline-bearing paragraph.
4038                let kids = self.children(id);
4039                // A block-level image: a paragraph (or other wrapper — a
4040                // `<picture>`, an `<h1>` banner) whose only visible content is a
4041                // single `image` node. Render it as a placeholder row + record an
4042                // [`MediaInfo`] a capable frontend replaces. An image mixed with
4043                // real text or other images on the line isn't block-level and
4044                // falls through to the inline path below, still as its alt text.
4045                if let Some((m, kind)) = self.media_only(id) {
4046                    self.block_media(m, kind, id, pf);
4047                    return;
4048                }
4049                // A display formula on lines of its own — a paragraph whose
4050                // only visible content is one `display_math` — promotes the
4051                // same way, to a placeholder row and a [`MathMark`].
4052                if let Some(m) = self.math_only(id) {
4053                    self.block_math(m, pf, pc);
4054                    return;
4055                }
4056                let inline = !kids.is_empty() && kids.iter().all(|&c| is_inline(&self.nodes[c]));
4057                if inline || kids.is_empty() {
4058                    // The block's own attributes over its containers' — the
4059                    // three run-level keys become the style its glyphs start
4060                    // from, and the two line-level ones ride every row it
4061                    // emits, a wrapped paragraph's continuations included.
4062                    let pres = self.presentation.under(&node.attrs, &self.faces);
4063                    let glyphs =
4064                        self.inline_children_with_trailing(id, pres.over(Style::default()));
4065                    if !glyphs.is_empty() {
4066                        let first = self.rows.len();
4067                        self.emit_wrapped(glyphs, node.span.start, pf, pc);
4068                        for row in &mut self.rows[first..] {
4069                            row.align = pres.align;
4070                            row.line_height = pres.line_height;
4071                        }
4072                    }
4073                } else {
4074                    self.blocks(id, pf, pc, false);
4075                }
4076            }
4077        }
4078    }
4079
4080    /// Render a table as a box-drawn grid: every column as wide as its widest
4081    /// cell, the header bold and ruled off, each cell padded to its column's
4082    /// alignment. This is the *default* monospace rendering (see
4083    /// [`VisualMap::rows`]); the same cells are also published structurally as
4084    /// [`TableInfo`], so a frontend that lays the grid out in its own units draws
4085    /// from there and skips the picture built here.
4086    ///
4087    /// The alignment comes from twig's `cell.alignment` — the delimiter row
4088    /// (`|:--|--:|`) that spells it out is consumed by the parser and leaves no
4089    /// node, so the snapshot is the only source for it.
4090    ///
4091    /// Borders and padding are *decoration*: they carry the source offset of the
4092    /// text they surround, so a click lands in that cell, but they're never
4093    /// caret stops — the caret steps cell-to-cell instead of into the box art.
4094    fn table(&mut self, id: usize, pf: &[Glyph], pc: &[Glyph]) {
4095        let node_end = self.nodes[id].span.end;
4096        // twig's shape is `[caption, row, row, …]`: the caption is always
4097        // present (usually empty in Markdown) and is not part of the grid.
4098        let row_ids: Vec<usize> = self
4099            .children(id)
4100            .into_iter()
4101            .filter(|&c| self.nodes[c].kind == Kind::Row)
4102            .collect();
4103        if row_ids.is_empty() {
4104            return;
4105        }
4106        // Lay every cell out first — the column widths depend on all of them.
4107        let grid: Vec<Vec<TableCell>> = row_ids.iter().map(|&r| self.row_cells(r)).collect();
4108        let heads: Vec<bool> = row_ids
4109            .iter()
4110            .map(|&r| self.nodes[r].head.unwrap_or(false))
4111            .collect();
4112        let cols = grid.iter().map(|r| r.len()).max().unwrap_or(0);
4113        if cols == 0 {
4114            return;
4115        }
4116        let mut widths = vec![0usize; cols];
4117        for row in &grid {
4118            for (c, cell) in row.iter().enumerate() {
4119                widths[c] = widths[c].max(cell_width(&cell.glyphs));
4120            }
4121        }
4122        // Every column at its widest cell is only the *wish*; a grid wider than
4123        // the surface has its far side hanging off the edge where no amount of
4124        // caret motion can reach it. Cut it down to what's actually there, and
4125        // let the cells wrap into the space they're given.
4126        if let Some(w) = self.wrap {
4127            fit_widths(&mut widths, w.saturating_sub(prefix_width(pc)));
4128        }
4129
4130        // Where the picture starts, so a frontend drawing its own grid knows
4131        // which rows to skip. Recorded before the first border goes down.
4132        let rows_start = self.rows.len();
4133
4134        let anchor = grid[0].first().map(|c| c.start).unwrap_or(node_end);
4135        self.push_rule(&rule_text(&widths, '┌', '┬', '┐'), anchor, pf);
4136        for (ri, row) in grid.iter().enumerate() {
4137            self.push_table_row(row, &widths, pc);
4138            // The rule under the header: only where the head actually ends.
4139            let ends_head = heads[ri] && heads.get(ri + 1) == Some(&false);
4140            if ends_head {
4141                let next = grid[ri + 1].first().map(|c| c.start).unwrap_or(node_end);
4142                self.push_rule(&rule_text(&widths, '├', '┼', '┤'), next, pc);
4143            }
4144        }
4145        // The bottom border is the one rule the caret can rest on: its end is
4146        // the table's trailing stop, the caret home just past the block — the
4147        // peer of a block picture's second stop, and of a rule's row end. Without
4148        // it a document ending in a table ended *inside* it: nothing after the
4149        // last cell was a stop, so Right could not leave the table, and a click
4150        // in the blank space under it snapped back into the last cell — or, on a
4151        // surface that resolved the click onto the border row, to the table's
4152        // first cell, since a decoration row's only stop is the nearest one.
4153        // Typing at the stop opens a paragraph first, as at a picture's — see
4154        // `Doc::open_paragraph_at_block_edge`. The glyphs stay non-stops at
4155        // `node_end`, so a click anywhere on the border lands past the table.
4156        self.push_rule_with_home(&rule_text(&widths, '└', '┴', '┘'), node_end, pc);
4157
4158        // The same cells the picture above was drawn from, published unwrapped
4159        // and unpadded for a frontend that lays them out in pixels.
4160        self.tables.push(TableInfo {
4161            rows_span: rows_start..self.rows.len(),
4162            end_src: node_end,
4163            // The *continuation* prefix: `pf` opens the block and only its first
4164            // row wears it, but every row of a grid is a continuation of the
4165            // block the table sits in.
4166            prefix: pc.to_vec(),
4167            grid: grid
4168                .into_iter()
4169                .zip(heads)
4170                .map(|(cells, head)| TableRow { head, cells })
4171                .collect(),
4172        });
4173        // The table's own end anchors whatever separator follows it; the border
4174        // rows deliberately don't move `last_off` (they hold no content).
4175        self.last_off = node_end;
4176    }
4177
4178    /// One row of laid-out cells, in column order.
4179    fn row_cells(&self, row: usize) -> Vec<TableCell> {
4180        // A cell is one source line, so a break within it is an explicit line
4181        // break (an inline `<br>`) that must render as a line of its own — not the
4182        // flow-folding space a break is in prose.
4183        self.break_glyph.set('\n');
4184        let cells = self
4185            .children(row)
4186            .into_iter()
4187            .filter(|&c| self.nodes[c].kind == Kind::Cell)
4188            .enumerate()
4189            .map(|(col, c)| {
4190                let n = &self.nodes[c];
4191                let style = if n.head.unwrap_or(false) {
4192                    Style::default().bold()
4193                } else {
4194                    Style::default()
4195                };
4196                // Only `content_span` bounds a cell's text, and an EMPTY cell
4197                // has none at all — twig records no interior for it — so both
4198                // offsets would fall back to the cell's `span.start`: on the
4199                // pipe that opens it, or (under a twig that gave every cell
4200                // the whole row's span) the row's start, where every empty
4201                // cell collapses onto one spot before the first `│` and a
4202                // caret there types *before* the table. Derive the interior
4203                // from the span's own pipes and this cell's column instead,
4204                // so each empty cell has a distinct, editable caret home.
4205                let span = n.content_span.clone().unwrap_or_else(|| {
4206                    let off = empty_cell_offset(
4207                        &self.source[n.span.start.min(self.source.len())
4208                            ..n.span.end.min(self.source.len())],
4209                        n.span.start,
4210                        col,
4211                    );
4212                    off..off
4213                });
4214                TableCell {
4215                    glyphs: self.inline_children(c, style),
4216                    start: span.start,
4217                    end: span.end,
4218                    align: n.alignment.unwrap_or(Alignment::Default),
4219                }
4220            })
4221            .collect();
4222        self.break_glyph.set(' ');
4223        cells
4224    }
4225
4226    /// A horizontal rule between/around rows — entirely decoration.
4227    fn push_rule(&mut self, text: &str, src: usize, prefix: &[Glyph]) {
4228        self.push_rule_row(text, src, prefix, true);
4229    }
4230
4231    /// A table's bottom border: drawn like the other rules, but a row the caret
4232    /// can rest on, its end (`src`) being the table's trailing stop.
4233    fn push_rule_with_home(&mut self, text: &str, src: usize, prefix: &[Glyph]) {
4234        self.push_rule_row(text, src, prefix, false);
4235    }
4236
4237    fn push_rule_row(&mut self, text: &str, src: usize, prefix: &[Glyph], decoration: bool) {
4238        let glyphs = concat(prefix, &synth(text, Role::Rule, src));
4239        self.rows.push(VRow {
4240            glyphs,
4241            end_src: src,
4242            decoration,
4243            code: false,
4244            code_lang: None,
4245            directive: false,
4246            directive_label: None,
4247            media: None,
4248            task: None,
4249            leaf_directive: None,
4250            heading: None,
4251            align: None,
4252            line_height: None,
4253            boundary: None,
4254            mark_ends: Vec::new(),
4255            math: Vec::new(),
4256        });
4257    }
4258
4259    /// One `│ a │ b │` row of the grid: real cell text between decoration.
4260    ///
4261    /// A row of cells is not a row of the screen — a cell wrapped to its column
4262    /// spans several, each one `│`-divided across the full width so the grid
4263    /// stays square. Cells in the same row are laid out independently and run
4264    /// out at their own heights; a column that has run dry pads out as
4265    /// decoration while its neighbours keep going.
4266    fn push_table_row(&mut self, cells: &[TableCell], widths: &[usize], prefix: &[Glyph]) {
4267        let fallback = cells.last().map(|c| c.end).unwrap_or(0);
4268        let laid: Vec<Vec<Vec<Glyph>>> = cells
4269            .iter()
4270            .enumerate()
4271            .map(|(ci, c)| wrap_glyphs(&c.glyphs, widths.get(ci).copied().unwrap_or(0)))
4272            .collect();
4273        let height = laid.iter().map(|l| l.len()).max().unwrap_or(1).max(1);
4274
4275        for j in 0..height {
4276            let mut glyphs = prefix.to_vec();
4277            for (ci, &w) in widths.iter().enumerate() {
4278                let cell = cells.get(ci);
4279                let line = laid.get(ci).and_then(|l| l.get(j));
4280                // The divider before this column belongs to the cell it
4281                // introduces, so clicking it lands in that cell — on this line
4282                // of it, which is what's next to the divider being clicked.
4283                let at = line
4284                    .and_then(|l| l.first().map(|g| g.src))
4285                    .or_else(|| cell.map(|c| c.start))
4286                    .unwrap_or(fallback);
4287                glyphs.extend(synth("│", Role::Rule, at));
4288                match (cell, line) {
4289                    (Some(cell), Some(line)) => {
4290                        let pad = w.saturating_sub(glyphs_width(line));
4291                        let (lead, trail) = match cell.align {
4292                            Alignment::Right => (pad, 0),
4293                            Alignment::Center => (pad / 2, pad - pad / 2),
4294                            Alignment::Left | Alignment::Default => (0, pad),
4295                        };
4296                        // Every line renders at least one space after its text
4297                        // (the gutter before `│`), so there is always somewhere
4298                        // to put the "after the last character" caret a line
4299                        // needs. It's the one padding glyph that is a stop: on
4300                        // the cell's last line that's the cell's end, and on any
4301                        // other it's the space the wrap consumed.
4302                        let last = laid[ci].len() == j + 1;
4303                        let end = match last {
4304                            true => cell.end,
4305                            false => line
4306                                .last()
4307                                .map(|g| g.src + g.ch.len_utf8())
4308                                .unwrap_or(cell.end),
4309                        };
4310                        glyphs.extend(synth(&" ".repeat(lead + 1), Role::Body, at));
4311                        glyphs.extend(line.iter().cloned());
4312                        glyphs.push(Glyph {
4313                            ch: ' ',
4314                            style: Style::default(),
4315                            src: end,
4316                            stop: true,
4317                        });
4318                        glyphs.extend(synth(&" ".repeat(trail), Role::Body, end));
4319                    }
4320                    // A ragged row, or a column whose cell ended higher up: pad
4321                    // it out so the grid stays square.
4322                    _ => {
4323                        let at = cell.map(|c| c.end).unwrap_or(fallback);
4324                        glyphs.extend(synth(&" ".repeat(w + 2), Role::Body, at));
4325                    }
4326                }
4327            }
4328            glyphs.extend(synth("│", Role::Rule, fallback));
4329            // The row ends where its last stop does. A table row has no gap
4330            // between its final cell and the border, so inventing an end past
4331            // that would be a stop with nothing under it.
4332            let end_src = glyphs
4333                .iter()
4334                .rev()
4335                .find(|g| g.stop)
4336                .map_or(fallback, |g| g.src);
4337            let mark_ends = self.take_mark_ends(end_src);
4338            let math = self.take_math(&glyphs);
4339            self.rows.push(VRow {
4340                glyphs,
4341                end_src,
4342                decoration: false,
4343                code: false,
4344                code_lang: None,
4345                directive: false,
4346                directive_label: None,
4347                media: None,
4348                task: None,
4349                leaf_directive: None,
4350                heading: None,
4351                align: None,
4352                line_height: None,
4353                boundary: None,
4354                mark_ends,
4355                math,
4356            });
4357        }
4358    }
4359
4360    /// Render a block-level image, video, or audio as one placeholder row: the
4361    /// `🖼 alt` / `🎬 alt` / `🔊 alt` label styled [`Role::Image`], every glyph
4362    /// mapped to the media's start offset and a caret stop there (they share the
4363    /// offset, so the stop table dedups them to a single home in front of it, as
4364    /// a rule's dashes do), and the row's end stop set past it so the caret can
4365    /// also rest after it. The row carries a [`MediaMark`] so [`media_spans`]
4366    /// publishes it as a [`MediaInfo`] a capable frontend replaces with the real
4367    /// picture or player; a plain surface paints the label as-is. `pf` is the
4368    /// block prefix (a list indent, a quote gutter) the row opens with, exactly
4369    /// as every other block honours it.
4370    fn block_media(&mut self, img: usize, kind: MediaKind, wrapper: usize, pf: &[Glyph]) {
4371        let node = &self.nodes[img];
4372        let start = node.span.start;
4373        let end = node.span.end;
4374        // An `image`'s URL is twig's `destination`; a `<video>`/`<audio>` is a
4375        // generic element, so its URL is the `src` attribute — and may be absent
4376        // entirely, the element naming its candidates in child `<source>`s.
4377        let destination = match kind {
4378            MediaKind::Image => node.destination.clone().unwrap_or_default(),
4379            MediaKind::Video | MediaKind::Audio => attr_of(node, "src").unwrap_or_default(),
4380        };
4381        let poster = match kind {
4382            MediaKind::Video => attr_of(node, "poster").unwrap_or_default(),
4383            MediaKind::Image | MediaKind::Audio => String::new(),
4384        };
4385        // The `<source>`s under the media element itself, not under `wrapper`: a
4386        // `<video>` is its own container, unlike an `<img>`, whose `<picture>`
4387        // alternatives are its *siblings* and so only reachable from the wrapper.
4388        let sources = match kind {
4389            MediaKind::Image => self.media_sources(wrapper),
4390            MediaKind::Video | MediaKind::Audio => self.media_sources(img),
4391        };
4392        let alt = self.image_alt(img);
4393        let sigil = kind.sigil();
4394        let label = if alt.is_empty() {
4395            // With no alt, name the file — but a `<video>` with neither `src` nor
4396            // alt has only its `<source>`s to be named by, so fall back to the
4397            // first candidate rather than labelling the row a bare sigil.
4398            let named = if destination.is_empty() {
4399                sources
4400                    .first()
4401                    .map(|s| s.srcset.as_str())
4402                    .unwrap_or_default()
4403            } else {
4404                &destination
4405            };
4406            format!("{sigil} {}", media_label(named))
4407        } else {
4408            format!("{sigil} {alt}")
4409        };
4410        let style = Style::default().role(Role::Image);
4411        let mut glyphs = pf.to_vec();
4412        for ch in label.chars() {
4413            glyphs.push(Glyph {
4414                ch,
4415                style,
4416                src: start,
4417                stop: true,
4418            });
4419        }
4420        // How many rows the frontend wants for this picture: the label row plus
4421        // the blank fillers below it. Absent (a GUI that lays images out in
4422        // pixels, an image that didn't resolve, or a plain surface) means the
4423        // bare one-row placeholder.
4424        let rows = self
4425            .surface
4426            .media_rows
4427            .get(&destination)
4428            .copied()
4429            .unwrap_or(1)
4430            .max(1);
4431        // End past the image so the caret has a stop after it: the last glyph's
4432        // offset is the image *start*, not its extent, so `push_row`'s
4433        // last-glyph rule would strand the end stop inside the markup.
4434        self.push_row_at(glyphs, end);
4435        if let Some(row) = self.rows.last_mut() {
4436            row.media = Some(MediaMark {
4437                kind,
4438                destination,
4439                sources,
4440                alt,
4441                poster,
4442                rows,
4443            });
4444        }
4445        // Reserve the picture's remaining height as blank `decoration` rows: drawn
4446        // (so the frontend has the vertical room to paint the raster over them),
4447        // but holding no caret and contributing no stops — vertical motion steps
4448        // over them and the caret's only homes stay the stop in front of the image
4449        // and the one just past it, both on the label row above. They anchor at the
4450        // image's end offset so a click on the picture's lower half lands after it,
4451        // the nearest caret home. Mirrors how a table's box-rule rows reserve space
4452        // without ever holding the caret.
4453        for _ in 1..rows {
4454            self.rows.push(VRow {
4455                glyphs: Vec::new(),
4456                end_src: end,
4457                decoration: true,
4458                code: false,
4459                code_lang: None,
4460                directive: false,
4461                directive_label: None,
4462                media: None,
4463                task: None,
4464                leaf_directive: None,
4465                heading: None,
4466                align: None,
4467                line_height: None,
4468                boundary: None,
4469                mark_ends: Vec::new(),
4470                math: Vec::new(),
4471            });
4472        }
4473        self.last_off = end;
4474    }
4475
4476    /// The `<picture>` alternatives inside block-image `wrapper`, in document
4477    /// order — every `<source>` element in its subtree. Empty when there's no
4478    /// `<picture>`. Each is a `<source>`'s `media` + `srcset`; core keeps them
4479    /// verbatim and picks none (see [`MediaSource`]). A `<source>` with no
4480    /// `srcset` is dropped (nothing to load); its `media` may be empty (an
4481    /// unconditional override), which a frontend treats as always-matching.
4482    ///
4483    /// It scans the wrapper's whole subtree (via the forward `first_child` /
4484    /// `next_sibling` links, the reliable ones) rather than the `<img>`'s parent,
4485    /// for two reasons. A `<picture>` reaches core in two shapes: twig promotes a
4486    /// block `<picture>` to an `element(picture)` wrapping `[source, img]`, but
4487    /// leaves an inline one's tags as raw siblings — `[raw "<picture>", source,
4488    /// img, raw "</picture>"]` — so the `<source>`s sit at different depths in
4489    /// the two. And the editor's flat arena leaves a promoted inline node's
4490    /// `parent` back-pointer dangling on a phantom root, so only the wrapper
4491    /// (known at the call site) is a trustworthy anchor. A block image is the
4492    /// sole visible content of its wrapper, so every `<source>` under it is its
4493    /// picture's.
4494    fn media_sources(&self, wrapper: usize) -> Vec<MediaSource> {
4495        let mut out = Vec::new();
4496        self.collect_sources(wrapper, &mut out);
4497        out
4498    }
4499
4500    fn collect_sources(&self, id: usize, out: &mut Vec<MediaSource>) {
4501        for c in self.children(id) {
4502            let node = &self.nodes[c];
4503            if node.name.as_deref() == Some("source") {
4504                // `<picture>` spells its candidate `srcset`, `<video>`/`<audio>`
4505                // spell it `src`. Both mean "the URL to load", so they normalise
4506                // onto one field; `srcset` wins where (illegally) both appear.
4507                let url = attr_of(node, "srcset").or_else(|| attr_of(node, "src"));
4508                if let Some(srcset) = url {
4509                    out.push(MediaSource {
4510                        media: attr_of(node, "media").unwrap_or_default(),
4511                        srcset,
4512                        mime: attr_of(node, "type").unwrap_or_default(),
4513                    });
4514                }
4515            }
4516            self.collect_sources(c, out);
4517        }
4518    }
4519
4520    /// The single block-level media `id`'s subtree resolves to, or `None`.
4521    ///
4522    /// A wrapper is a block picture when the only *visible* thing under it is one
4523    /// image: whitespace-only text and structure-only elements (a `<picture>`'s
4524    /// `<source>`, which declares an alternate but paints nothing) don't count,
4525    /// and the search descends through wrapping elements (`<picture>`, a linking
4526    /// `<a>`). This is what makes `<p><img></p>`, a bare `<img>`, and
4527    /// `<h1><picture>…<img></picture></h1>` all render as one framed picture.
4528    /// Any real text, or a second image, means it isn't image-only — it falls
4529    /// back to inline rendering, where the image still shows as its alt text.
4530    ///
4531    /// [`FlatNode`]'s snapshot doesn't carry an element's tag name, so a
4532    /// `<source>` can't be skipped by name — but it needs no special case:
4533    /// contributing no image and no text, it's simply invisible to the scan.
4534    fn media_only(&self, id: usize) -> Option<(usize, MediaKind)> {
4535        let mut found = None;
4536        let mut count = 0usize;
4537        let mut has_text = false;
4538        self.scan_visual(id, &mut found, &mut count, &mut has_text);
4539        (count == 1 && !has_text).then(|| found.unwrap())
4540    }
4541
4542    /// Walk `id`'s subtree tallying visible leaves for [`media_only`]: each
4543    /// image, `<video>`, or `<audio>` (remembering the last, counting the total)
4544    /// and whether any non-whitespace text appears. Media isn't descended into —
4545    /// an image's inline children are alt text, and a `<video>`'s are its
4546    /// no-support fallback and its `<source>` declarations, none of which is
4547    /// document content.
4548    ///
4549    /// [`media_only`]: Self::media_only
4550    fn scan_visual(
4551        &self,
4552        id: usize,
4553        found: &mut Option<(usize, MediaKind)>,
4554        count: &mut usize,
4555        has_text: &mut bool,
4556    ) {
4557        for c in self.children(id) {
4558            let node = &self.nodes[c];
4559            match node.kind.as_str() {
4560                "image" => {
4561                    *found = Some((c, MediaKind::Image));
4562                    *count += 1;
4563                }
4564                // A `<video>`/`<audio>` reaches core as a generic `container`
4565                // (twig gives neither a semantic node, so `html_elements`
4566                // promotion leaves the tag name on `name`). Counted as media and
4567                // *not* descended into, so its `<source>` children and its
4568                // "your browser does not support…" fallback text neither add a
4569                // second count nor make the block look like text.
4570                "container" if matches!(element_tag(node), Some("video") | Some("audio")) => {
4571                    let kind = match element_tag(node) {
4572                        Some("audio") => MediaKind::Audio,
4573                        _ => MediaKind::Video,
4574                    };
4575                    *found = Some((c, kind));
4576                    *count += 1;
4577                }
4578                // Text leaves: only non-whitespace counts as visible content.
4579                // (Twig keeps the whitespace `str`s between HTML tags — the
4580                // newlines and indentation inside a `<picture>` — as real nodes.)
4581                "str" | "smart_punctuation" | "verbatim" | "inline_math" | "display_math" => {
4582                    if node.text.as_deref().is_some_and(|t| !t.trim().is_empty()) {
4583                        *has_text = true;
4584                    }
4585                }
4586                // Structural breaks carry no visible glyph of their own.
4587                "soft_break" | "hard_break" | "non_breaking_space" => {}
4588                // Any other wrapper (emphasis, a link, a `<picture>`) is
4589                // transparent to the scan — descend into it.
4590                _ => self.scan_visual(c, found, count, has_text),
4591            }
4592        }
4593    }
4594
4595    /// The single `display_math` that is all of `id`'s visible content, or
4596    /// `None` — [`media_only`](Self::media_only) for a formula. Whitespace-only
4597    /// text around it does not count (twig keeps the newlines either side of a
4598    /// `$$` on its own lines as `str`s); any other text, or a second formula,
4599    /// means the paragraph is prose with math in it, and falls through to the
4600    /// inline path.
4601    fn math_only(&self, id: usize) -> Option<usize> {
4602        let mut found = None;
4603        let mut count = 0usize;
4604        for c in self.children(id) {
4605            let node = &self.nodes[c];
4606            match node.kind.as_str() {
4607                "display_math" => {
4608                    found = Some(c);
4609                    count += 1;
4610                }
4611                "str" | "smart_punctuation" => {
4612                    if node.text.as_deref().is_some_and(|t| !t.trim().is_empty()) {
4613                        return None;
4614                    }
4615                }
4616                "soft_break" | "hard_break" | "non_breaking_space" => {}
4617                _ => return None,
4618            }
4619        }
4620        (count == 1).then(|| found.unwrap())
4621    }
4622
4623    /// A display formula on lines of its own, drawn on
4624    /// [`block_media`](Self::block_media)'s recipe: one placeholder row —
4625    /// `∑` and the TeX on one line, every glyph [`Role::Math`] at the
4626    /// formula's start and a caret stop there, the row ending past the
4627    /// formula so the caret can rest after it too — carrying a [`MathMark`]
4628    /// for [`math_spans`] to publish, and under it as many blank decoration
4629    /// rows as the frontend said the picture is tall
4630    /// ([`Surface::math_rows`]).
4631    ///
4632    /// On the caret's line the block is its source instead: the `$$`
4633    /// delimiters in [`Role::Delimiter`] and the TeX between them in
4634    /// [`Role::Code`], line for line, the way a fence draws — so a formula is
4635    /// edited where it stands and folds back to its picture when the caret
4636    /// leaves. That is the same rule an inline formula follows, and it is
4637    /// what makes a block-level formula need no equivalent of
4638    /// [`VisualMap::block_media_stop`]: both of the placeholder's caret homes
4639    /// are on the formula's own lines, and standing on either reveals it.
4640    fn block_math(&mut self, math: usize, pf: &[Glyph], pc: &[Glyph]) {
4641        self.saw_math.set(true);
4642        let node = &self.nodes[math];
4643        let (start, end) = (node.span.start, node.span.end);
4644        let tex = node.text.clone().unwrap_or_default();
4645        if self.math_revealed(&node.span) {
4646            let mut glyphs = Vec::new();
4647            self.inline_verbatim(math, Style::default(), &mut glyphs, true);
4648            self.emit_wrapped(glyphs, start, pf, pc);
4649            self.last_off = end;
4650            return;
4651        }
4652        let style = Style::default().role(Role::Math);
4653        let mut glyphs = pf.to_vec();
4654        // One line of label: the TeX with its newlines folded, so the row
4655        // reads as one thing however the source laid it out.
4656        let label: String = tex.split_whitespace().collect::<Vec<_>>().join(" ");
4657        for ch in format!("{MATH_ATOM} {label}").chars() {
4658            glyphs.push(Glyph {
4659                ch,
4660                style,
4661                src: start,
4662                stop: true,
4663            });
4664        }
4665        let rows = self
4666            .surface
4667            .math_rows
4668            .get(&tex)
4669            .copied()
4670            .unwrap_or(1)
4671            .max(1);
4672        self.push_row_at(glyphs, end);
4673        if let Some(row) = self.rows.last_mut() {
4674            row.math = vec![MathMark {
4675                tex,
4676                display: true,
4677                glyph: None,
4678                rows,
4679            }];
4680        }
4681        for _ in 1..rows {
4682            self.rows.push(VRow {
4683                glyphs: Vec::new(),
4684                end_src: end,
4685                decoration: true,
4686                code: false,
4687                code_lang: None,
4688                directive: false,
4689                directive_label: None,
4690                media: None,
4691                task: None,
4692                leaf_directive: None,
4693                heading: None,
4694                align: None,
4695                line_height: None,
4696                boundary: None,
4697                mark_ends: Vec::new(),
4698                math: Vec::new(),
4699            });
4700        }
4701        self.last_off = end;
4702    }
4703
4704    /// A leaf directive (`::name{…}`) as one placeholder row — the
4705    /// [`block_media`](Self::block_media) recipe, for the same reason: it is a
4706    /// block that renders as *a thing*, not as text, and the frontend paints
4707    /// whatever the host app's vocabulary makes of it.
4708    ///
4709    /// The row's glyphs are a `⧉ label` (or `⧉ name`) stand-in a plain surface
4710    /// paints as-is, every glyph anchored at the directive's start with a caret
4711    /// stop there, and the row ending past it so the caret can also rest after
4712    /// it. It carries a [`DirectiveMark`] for [`directive_spans`], and is marked
4713    /// [`directive`](VRow::directive) so a frontend already drawing the
4714    /// container form's panel frames this one identically for free.
4715    ///
4716    /// Before this, a leaf directive emitted no rows at all: it was invisible,
4717    /// held no caret, and vertical motion crossed a void where it stood.
4718    fn block_directive(&mut self, id: usize, pf: &[Glyph]) {
4719        let node = &self.nodes[id];
4720        let (start, end) = (node.span.start, node.span.end);
4721        let (name, attrs) = leaf_directive_identity(node);
4722        let label = self.image_alt(id); // its `[label]` children, flattened
4723        let shown = if label.is_empty() { &name } else { &label };
4724        let style = Style::default().role(Role::Image);
4725        let mut glyphs = pf.to_vec();
4726        for ch in format!("⧉ {shown}").chars() {
4727            glyphs.push(Glyph {
4728                ch,
4729                style,
4730                src: start,
4731                stop: true,
4732            });
4733        }
4734        // End past the directive so the caret has a stop after it — the same
4735        // reason `block_media` anchors its row at the image's end.
4736        self.push_row_at(glyphs, end);
4737        if let Some(row) = self.rows.last_mut() {
4738            row.directive = true;
4739            row.leaf_directive = Some(DirectiveMark {
4740                name,
4741                attrs,
4742                label,
4743                rows: 1,
4744            });
4745        }
4746        self.last_off = end;
4747    }
4748
4749    /// An image's alt text: the flattened text of its inline descendants (an
4750    /// image's children *are* its alt content), empty when it has none. Also a
4751    /// leaf directive's `[label]`, which is the same shape — inline children
4752    /// standing for the block.
4753    fn image_alt(&self, id: usize) -> String {
4754        let mut out = String::new();
4755        self.collect_text(id, &mut out);
4756        out
4757    }
4758
4759    /// Append every descendant's `text` to `out`, in document order. Inline text
4760    /// (`str`) nodes are leaves, so a node never contributes both its own text and
4761    /// a child's — no double counting.
4762    fn collect_text(&self, id: usize, out: &mut String) {
4763        for c in self.children(id) {
4764            if let Some(t) = &self.nodes[c].text {
4765                out.push_str(t);
4766            }
4767            self.collect_text(c, out);
4768        }
4769    }
4770
4771    fn inline_children(&self, id: usize, base: Style) -> Vec<Glyph> {
4772        let mut out = Vec::new();
4773        for c in self.children(id) {
4774            self.inline(c, base, &mut out);
4775        }
4776        out
4777    }
4778
4779    /// [`inline_children`](Self::inline_children) plus any trailing whitespace the
4780    /// block carries past its inline content (see [`trailing_ws_glyphs`]). Used
4781    /// for the leaf inline blocks — paragraphs and headings — whose own `span`
4782    /// bounds exactly one line of text, so the trailing gap is theirs. *Not* for
4783    /// a table cell, whose `span` is the whole row and would swallow the
4784    /// delimiters and neighbours between it and the row's end.
4785    ///
4786    /// [`trailing_ws_glyphs`]: Self::trailing_ws_glyphs
4787    fn inline_children_with_trailing(&self, id: usize, base: Style) -> Vec<Glyph> {
4788        let mut out = self.inline_children(id, base);
4789        out.extend(self.trailing_ws_glyphs(id, base));
4790        out
4791    }
4792
4793    /// Glyphs for whatever trailing whitespace a block's source carries past its
4794    /// last inline node — the space(s) at the end of `hello ` that Markdown and
4795    /// Djot drop from the `str` node as insignificant. twig still records them:
4796    /// a block's `content_span` ends at its last meaningful character while its
4797    /// `span` runs to the end of the line's text (before the terminating
4798    /// newline), so the gap between the two *is* that trailing whitespace.
4799    ///
4800    /// Emitting it as real caret-stop glyphs is what lets the caret be drawn
4801    /// past the last visible character. Without it, typing a space at the end of
4802    /// a paragraph moved the caret in the source but not on screen — the caret
4803    /// stuck on the last glyph until the next visible character reparsed the
4804    /// space into an interior `str` node that finally carried it.
4805    ///
4806    /// Restricted to spaces: only they are safe to synthesize one-cell-per-byte,
4807    /// and only they are what the parser silently strips. Anything else in the
4808    /// gap means the span accounting isn't what this assumes, so it's left alone.
4809    fn trailing_ws_glyphs(&self, id: usize, style: Style) -> Vec<Glyph> {
4810        let node = &self.nodes[id];
4811        let Some(content) = &node.content_span else {
4812            return Vec::new();
4813        };
4814        let (from, to) = (content.end, node.span.end);
4815        let Some(slice) = (from < to).then(|| self.source.get(from..to)).flatten() else {
4816            return Vec::new();
4817        };
4818        if slice.is_empty() || slice.bytes().any(|b| b != b' ') {
4819            return Vec::new();
4820        }
4821        slice
4822            .bytes()
4823            .enumerate()
4824            .map(|(i, _)| Glyph {
4825                ch: ' ',
4826                style,
4827                src: from + i,
4828                stop: true,
4829            })
4830            .collect()
4831    }
4832
4833    fn inline(&self, id: usize, base: Style, out: &mut Vec<Glyph>) {
4834        let node = &self.nodes[id];
4835        match node.kind.as_str() {
4836            "str" | "smart_punctuation" => push_escaped_text(
4837                out,
4838                node.text.as_deref().unwrap_or(""),
4839                node.span.clone(),
4840                self.source,
4841                base,
4842            ),
4843            "soft_break" | "hard_break" | "non_breaking_space" => {
4844                // A break renders as a real, caret-navigable glyph — but twig
4845                // gives it no span of its own (`0..0`), so the offset comes from
4846                // the text in front of it: one *past* the last glyph, which is
4847                // the newline the break stands for. Past, not on: sharing the
4848                // previous glyph's offset would put two stops on one byte, and a
4849                // caret that can't change offset can't move.
4850                let src = if node.span.start != 0 {
4851                    node.span.start
4852                } else {
4853                    out.last().map(|g| g.src + g.ch.len_utf8()).unwrap_or(0)
4854                };
4855                // A *hard* break renders as this run's break glyph — a newline
4856                // inside a table cell (its own line), the same space in prose the
4857                // frontend re-wraps. A soft break normally folds into a space;
4858                // under `LineFlow::Preserve` it renders as a `'\n'` too, so the
4859                // author's line break shows where it was written. Never inside a
4860                // cell (`break_glyph` is `'\n'` there): a cell is one line and
4861                // folds its own soft breaks regardless.
4862                let ch = if node.kind == Kind::HardBreak {
4863                    self.break_glyph.get()
4864                } else if node.kind == Kind::SoftBreak
4865                    && self.preserve_soft
4866                    && self.break_glyph.get() == ' '
4867                {
4868                    '\n'
4869                } else {
4870                    ' '
4871                };
4872                out.push(Glyph {
4873                    ch,
4874                    style: base,
4875                    src,
4876                    stop: true,
4877                });
4878            }
4879            // A cell's only spelling for an in-line break is a raw `<br>`; read it
4880            // back as one (outside a cell it stays the literal text it falls to
4881            // below). The tag's bytes carry no stop of their own — the line it
4882            // ends stops just before it, the next just after.
4883            "raw_inline" if self.break_glyph.get() == '\n' && is_br(node.text.as_deref()) => {
4884                out.push(Glyph {
4885                    ch: '\n',
4886                    style: base,
4887                    src: node.span.start,
4888                    stop: true,
4889                });
4890            }
4891            "emph" => self.inline_delimited(id, base.italic(), out),
4892            "strong" => self.inline_delimited(id, base.bold(), out),
4893            // A coloured highlight's emoji is spelling, not content: twig strips
4894            // it and records the colour on the node, so the glyphs are the
4895            // author's words and the colour rides the role. Revealed markup
4896            // still shows the emoji, because `delims` reads the source bytes
4897            // between the span and the content span — which is exactly the
4898            // `==🔴 ` the author typed.
4899            "mark" => {
4900                let color = MarkColor::from_attrs(&node.attrs);
4901                self.inline_delimited(id, base.role(Role::Mark(color)), out)
4902            }
4903            "insert" => self.inline_delimited(id, base.underline(), out),
4904            "delete" => self.inline_delimited(id, base.strikethrough(), out),
4905            // The one pair whose whole meaning is *where the glyphs sit*. Drawn
4906            // in the surrounding style otherwise, so `^**2**^` stays bold and a
4907            // superscript inside a heading keeps the heading's role — which is
4908            // exactly why this is a `Baseline` and not a `Role`.
4909            "superscript" => self.inline_delimited(id, base.baseline(Baseline::Super), out),
4910            "subscript" => self.inline_delimited(id, base.baseline(Baseline::Sub), out),
4911            "verbatim" => self.inline_verbatim(id, base, out, self.revealed(&node.span)),
4912            // A formula in a line. Three renderings, in order of preference:
4913            //
4914            // - On the caret's line it is its TeX in the code style with its
4915            //   delimiters shown — in *every* markup mode, because the
4916            //   content is not the picture and hiding the `$` alone would
4917            //   leave nothing to edit. `math_revealed` is `revealed` without
4918            //   the mode gate.
4919            // - On a surface that paints pictures in a line it is one atom
4920            //   glyph, `stop: true` at the formula's start, standing for the
4921            //   whole thing; the [`MathMark`] drained onto the row by
4922            //   [`take_math`](Self::take_math) says what the frontend draws
4923            //   there. The caret has the stop on the atom and the next glyph's
4924            //   past it, and nothing inside the markup.
4925            // - Elsewhere — a terminal — it is the code-styled TeX with the
4926            //   delimiters hidden, exactly the verbatim treatment, and what
4927            //   `inline_math` rendered as before there was anything else.
4928            //   `display_math` had no arm at all and fell to the default one,
4929            //   which pushed its text at the *node's* start, three bytes short
4930            //   of where the text sits.
4931            "inline_math" | "display_math" => {
4932                self.saw_math.set(true);
4933                if self.math_revealed(&node.span) {
4934                    self.inline_verbatim(id, base, out, true);
4935                } else if self.surface.inline_pictures {
4936                    let tex = node.text.clone().unwrap_or_default();
4937                    self.pending_math.borrow_mut().push((
4938                        node.span.start,
4939                        MathMark {
4940                            tex,
4941                            display: node.kind == Kind::DisplayMath,
4942                            glyph: None,
4943                            rows: 1,
4944                        },
4945                    ));
4946                    out.push(Glyph {
4947                        ch: MATH_ATOM,
4948                        style: base.role(Role::Math),
4949                        src: node.span.start,
4950                        stop: true,
4951                    });
4952                } else {
4953                    self.inline_verbatim(id, base, out, false);
4954                }
4955            }
4956            // An attributed span — the run-level half of the presentation
4957            // vocabulary. djot's `[text]{…}`, AsciiDoc's `[.a]#text#`, HTML's
4958            // and Markdown's `<span …>`: one node with a name twig hands back
4959            // for two of the four (see [`is_run_span`]), all four carrying the
4960            // author's `data-size`, `data-font` and `data-color` on the run
4961            // they cover.
4962            //
4963            // The keys are written over the surrounding style rather than
4964            // replacing it, so a span inside a block that names its own size
4965            // wins on size and keeps the block's face — the nearest-wins rule
4966            // the block walker applies through a `div`. A key the span does not
4967            // name is one the block still says.
4968            //
4969            // A `data-color` here is the text's *foreground*, where the same key
4970            // on a `mark` is a highlight's background: same vocabulary, same
4971            // enum, and no collision, because a `mark` is a `mark` and a span is
4972            // a span.
4973            //
4974            // Otherwise this is the plain `recurse` an anonymous container has
4975            // always had — no delimiters, because the `{…}` is markup and the
4976            // span's text is the author's words.
4977            "container" if is_run_span(node) && !self.children(id).is_empty() => {
4978                self.recurse(id, run_style(node, base, &self.faces), out)
4979            }
4980            // A text directive (`:name[label]{…}`) — the inline form of a generic
4981            // directive. Its `[label]` children are the visible text; the name and
4982            // the `{…}` attributes are the host app's vocabulary (diaryx's
4983            // `:vis[…]`) and stay hidden markup, exactly as a link's `](dest)` is.
4984            // Drawn in the surrounding style: a role of its own would need one
4985            // every frontend maps, and the bug this fixes is that the text was
4986            // invisible, not that it was unstyled.
4987            "container" if container_is_directive(node) && !self.children(id).is_empty() => {
4988                self.recurse(id, base, out)
4989            }
4990            // No `[label]`, so there are no children to render and recursing
4991            // emitted *nothing*: the directive's bytes vanished from the document
4992            // and left no caret stop behind. What to draw instead turns on
4993            // whether the syntax looks deliberate.
4994            //
4995            // Bare `:word` almost never is. twig matches a colon followed by any
4996            // letter-led word (`scanTextDirective`, deliberately matching remark),
4997            // so ordinary prose is full of them — `:see below`, a `:smile:`
4998            // shortcode, a stray colon before a word. Those are prose, and prose
4999            // renders as itself: every byte visible, every byte a caret stop, so a
5000            // colon typed by accident can be seen and deleted. Hiding them behind
5001            // a placeholder would be the invisible-and-unreachable failure this
5002            // arm exists to fix, just wearing a nicer glyph.
5003            "container" if container_is_directive(node) && node.attrs.is_empty() => {
5004                let span = node.span.clone();
5005                push_text(
5006                    out,
5007                    self.source.get(span.clone()).unwrap_or(""),
5008                    span.start,
5009                    base,
5010                );
5011            }
5012            // `{…}` attributes, though, are unmistakably deliberate — nobody
5013            // types `:vis{.family}` by accident, and diaryx writes exactly that
5014            // inline. So an attribute-bearing directive with no label draws as a
5015            // chip on `block_directive`'s recipe (`⧉ name attrs`, `Role::Image`),
5016            // the inline peer of the leaf form's placeholder row.
5017            //
5018            // Only the first glyph is a caret stop, and the whole chip shares the
5019            // directive's start offset: the caret treats it as one atomic thing
5020            // rather than walking hidden markup a byte at a time, and a paragraph
5021            // holding nothing but a chip still has a stop to be navigated to.
5022            "container" if container_is_directive(node) => {
5023                let start = node.span.start;
5024                let name = node.name.clone().unwrap_or_default();
5025                let shown = match directive_attr_label(&node.attrs) {
5026                    Some(attrs) if !name.is_empty() => format!("⧉ {name} {attrs}"),
5027                    Some(attrs) => format!("⧉ {attrs}"),
5028                    None => format!("⧉ {name}"),
5029                };
5030                let style = base.role(Role::Image);
5031                for (i, ch) in shown.chars().enumerate() {
5032                    out.push(Glyph {
5033                        ch,
5034                        style,
5035                        src: start,
5036                        stop: i == 0,
5037                    });
5038                }
5039            }
5040            // A footnote reference (`[^1]`). The label bracketed is what a reader
5041            // needs — bare, `note1` reads as a typo rather than a reference — so
5042            // the `^` is hidden as the spelling artefact it is (a link's
5043            // `](dest)` goes the same way) and the brackets are kept as
5044            // decoration: one shared offset, never a caret stop, like a table's
5045            // borders, so the caret walks the label alone.
5046            //
5047            // Styled `Role::Link`: a reference *is* a link to its definition, and
5048            // every frontend already paints that role. A role of its own would
5049            // need one in each of them, and what a frontend needs to tell the two
5050            // apart is not a paint colour but an answer to "what does clicking
5051            // here do" — which is [`Doc::footnote_at_caret`]'s job, not a glyph's.
5052            //
5053            // Raised, though, because that a reference is *set* differently from
5054            // the prose it interrupts is exactly what makes it read as a
5055            // reference. `[1]` at body size reads as bracketed text.
5056            "footnote_reference" => {
5057                let style = base.role(Role::Link);
5058                // Revealed, the reference is just its source bytes: the `^` that
5059                // is normally elided comes back and every byte becomes a real
5060                // stop, so the brackets stop being decoration and start being
5061                // text. That's the whole point of the mode, and it replaces the
5062                // hand-built chip below rather than decorating it — including the
5063                // raised baseline, since what's on screen there is source, and
5064                // source is set as prose.
5065                if self.revealed(&node.span) {
5066                    self.push_delim(out, &node.span, style);
5067                    return;
5068                }
5069                let style = style.baseline(Baseline::Super);
5070                // The label's own span, so its glyphs map to their true bytes.
5071                // Absent one, it starts past the `[^` that opens the reference.
5072                let (label, at) = match &node.content_span {
5073                    Some(c) => (self.source.get(c.clone()).unwrap_or(""), c.start),
5074                    None => (node.text.as_deref().unwrap_or(""), node.span.start + 2),
5075                };
5076                out.push(Glyph {
5077                    ch: '[',
5078                    style,
5079                    src: node.span.start,
5080                    stop: false,
5081                });
5082                push_text(out, label, at, style);
5083                out.push(Glyph {
5084                    ch: ']',
5085                    style,
5086                    src: node.span.end.saturating_sub(1),
5087                    stop: false,
5088                });
5089            }
5090            "link" | "url" | "email" => {
5091                let style = base.role(Role::Link);
5092                if self.children(id).is_empty() {
5093                    // A bare autolink (`<a@b.c>`, a naked URL): the destination
5094                    // *is* the visible text, so there is nothing elided to
5095                    // reveal and both modes draw the same thing.
5096                    push_text(
5097                        out,
5098                        node.destination
5099                            .as_deref()
5100                            .or(node.text.as_deref())
5101                            .unwrap_or("link"),
5102                        node.span.start,
5103                        style,
5104                    );
5105                } else {
5106                    // An inline link reveals asymmetrically — `[` before the
5107                    // label, `](dest)` after it — which the generic
5108                    // span-minus-content derivation already produces.
5109                    self.inline_delimited(id, style, out);
5110                }
5111            }
5112            _ => {
5113                if self.children(id).is_empty() {
5114                    if let Some(t) = &node.text {
5115                        push_text(out, t, node.span.start, base);
5116                    }
5117                } else {
5118                    self.recurse(id, base, out);
5119                }
5120            }
5121        }
5122    }
5123
5124    fn recurse(&self, id: usize, style: Style, out: &mut Vec<Glyph>) {
5125        for c in self.children(id) {
5126            self.inline(c, style, out);
5127        }
5128    }
5129
5130    /// Lay a block's inline `glyphs` into visual rows, prefixing the first with
5131    /// `pf` and the rest with `pc`. A preserved soft break arrives as a `'\n'`
5132    /// glyph (see the `soft_break` arm): a hard row boundary that splits the
5133    /// glyphs so each run lays out on its own and the author's line structure
5134    /// shows on screen. The `'\n'` is dropped from the row it closes and its
5135    /// source offset becomes that row's end stop — exactly how a table cell's
5136    /// in-line `<br>` is handled — so the caret can rest at the line's end
5137    /// without a zero-width control char leaking into what the frontends render.
5138    /// With no `'\n'` present (the folding default, and every build that isn't
5139    /// `LineFlow::Preserve`) there is one run and this is byte-identical to
5140    /// laying the glyphs out directly.
5141    fn emit_wrapped(&mut self, glyphs: Vec<Glyph>, block_start: usize, pf: &[Glyph], pc: &[Glyph]) {
5142        if !glyphs.iter().any(|g| g.ch == '\n') {
5143            self.emit_line(glyphs, block_start, pf, pc, None);
5144            return;
5145        }
5146        // Each run up to a '\n' is a line of its own: the first wears the block's
5147        // opening prefix, every later one the continuation prefix, and the break's
5148        // own offset ends the run's last row. The break glyph is dropped. A
5149        // trailing '\n' flushes its run and leaves nothing behind, so no spurious
5150        // blank row follows it.
5151        let mut run: Vec<Glyph> = Vec::new();
5152        let mut first = true;
5153        for g in glyphs {
5154            if g.ch == '\n' {
5155                let lead = if first { pf } else { pc };
5156                self.emit_line(std::mem::take(&mut run), block_start, lead, pc, Some(g.src));
5157                first = false;
5158            } else {
5159                run.push(g);
5160            }
5161        }
5162        if !run.is_empty() {
5163            let lead = if first { pf } else { pc };
5164            self.emit_line(run, block_start, lead, pc, None);
5165        }
5166    }
5167
5168    /// Word-wrap a single line of `glyphs` (no interior line breaks) to the
5169    /// available width and push the visual rows, prefixing the first with `pf`
5170    /// and the rest with `pc`. `end`, when set, is the source offset that ends
5171    /// the line's final row — the offset of the break that terminated it, which
5172    /// the caller has already stripped from `glyphs`; when `None` the row ends
5173    /// just past its last glyph, as an unbroken block's does.
5174    fn emit_line(
5175        &mut self,
5176        glyphs: Vec<Glyph>,
5177        block_start: usize,
5178        pf: &[Glyph],
5179        pc: &[Glyph],
5180        end: Option<usize>,
5181    ) {
5182        // The line's final row ends at `end` when a break gave one, else just
5183        // past its last glyph (`push_row`'s default).
5184        let push_last = |b: &mut Self, row: Vec<Glyph>| match end {
5185            Some(e) => b.push_row_at(row, e),
5186            None => b.push_row(row, block_start),
5187        };
5188
5189        // No column budget: emit the whole line as one row and let the frontend
5190        // wrap it at its own (pixel) width.
5191        let Some(width) = self.wrap else {
5192            let row = if glyphs.is_empty() {
5193                pf.to_vec()
5194            } else {
5195                concat(pf, &glyphs)
5196            };
5197            push_last(self, row);
5198            return;
5199        };
5200
5201        // Split into words (maximal non-space runs), each carrying the space
5202        // glyph that followed it (so its source offset is preserved).
5203        let mut words: Vec<(Vec<Glyph>, Option<Glyph>)> = Vec::new();
5204        let mut word: Vec<Glyph> = Vec::new();
5205        for g in glyphs {
5206            if g.ch == ' ' {
5207                words.push((std::mem::take(&mut word), Some(g)));
5208            } else {
5209                word.push(g);
5210            }
5211        }
5212        if !word.is_empty() {
5213            words.push((word, None));
5214        }
5215        if words.is_empty() {
5216            // An empty block (or an empty preserved line) still occupies one
5217            // (prefixed) row.
5218            push_last(self, pf.to_vec());
5219            return;
5220        }
5221
5222        let mut line: Vec<Glyph> = Vec::new();
5223        let mut used = 0usize;
5224        let mut first = true;
5225        for (w, space) in words {
5226            let avail = width
5227                .saturating_sub(prefix_width(if first { pf } else { pc }))
5228                .max(1);
5229            let cells = glyphs_width(&w);
5230            if used > 0 && used + cells > avail {
5231                let row = concat(if first { pf } else { pc }, &line);
5232                self.push_row(row, block_start);
5233                line = Vec::new();
5234                used = 0;
5235                first = false;
5236            }
5237            used += cells;
5238            line.extend(w);
5239            if let Some(sp) = space {
5240                used += 1;
5241                line.push(sp);
5242            }
5243        }
5244        let row = concat(if first { pf } else { pc }, &line);
5245        push_last(self, row);
5246    }
5247
5248    /// The source offset of each line of a code block's `text`.
5249    ///
5250    /// `content` is the block's `content_span` — where twig says the body lives
5251    /// in the source, fences already excluded. Its lines run 1:1 with the
5252    /// rendered `text` lines, so no search is needed; each is anchored at the
5253    /// *end* of its source line, which places it past whatever indent `text` had
5254    /// stripped (a fenced block's fences, an indented one's leading spaces)
5255    /// without having to know how much there was.
5256    ///
5257    /// `None` when the body and the rendered lines don't line up — a coarse
5258    /// fallback the caller turns into the block's start offset.
5259    fn code_line_offsets(&self, content: &Range<usize>, lines: &[&str]) -> Option<Vec<usize>> {
5260        let mut src_lines: Vec<(usize, &str)> = Vec::new();
5261        let mut at = content.start;
5262        for l in self.source.get(content.start..content.end)?.split('\n') {
5263            src_lines.push((at, l));
5264            at += l.len() + 1;
5265        }
5266        if src_lines.len() != lines.len() {
5267            return None;
5268        }
5269        Some(
5270            lines
5271                .iter()
5272                .zip(&src_lines)
5273                .map(|(l, (start, sl))| start + sl.len().saturating_sub(l.len()))
5274                .collect(),
5275        )
5276    }
5277
5278    fn push_row(&mut self, glyphs: Vec<Glyph>, fallback: usize) {
5279        // Step past the character the *source* holds at the last glyph's offset,
5280        // not past the glyph's own `ch`. The two agree for ordinary text, but a
5281        // glyph is not always the character it stands on: `synth` decoration and
5282        // a substituted run (an image's `⧉ label`) share one offset by design.
5283        // Trusting `ch` there yields an offset inside a multi-byte character,
5284        // which every later slice of `source` panics on.
5285        let end_src = glyphs
5286            .last()
5287            .map(|g| {
5288                let at = g.src.min(self.source.len());
5289                at + self.source[at..].chars().next().map_or(0, char::len_utf8)
5290            })
5291            .unwrap_or(fallback);
5292        self.push_row_at(glyphs, end_src);
5293    }
5294
5295    /// Push a row with an explicit end stop, for content that knows its own
5296    /// extent better than its last glyph does.
5297    fn push_row_at(&mut self, glyphs: Vec<Glyph>, end_src: usize) {
5298        self.last_off = end_src;
5299        let mark_ends = self.take_mark_ends(end_src);
5300        let math = self.take_math(&glyphs);
5301        self.rows.push(VRow {
5302            glyphs,
5303            end_src,
5304            decoration: false,
5305            code: false,
5306            code_lang: None,
5307            directive: false,
5308            directive_label: None,
5309            media: None,
5310            task: None,
5311            leaf_directive: None,
5312            heading: None,
5313            align: None,
5314            line_height: None,
5315            boundary: None,
5316            mark_ends,
5317            math,
5318        });
5319    }
5320
5321    /// Where the last row's line ends — what the lines under it are counted
5322    /// from: the row's own end, or the walk's where that stands short of it.
5323    /// Only a thematic break's does, whose row ends at the caret's home past
5324    /// the newline under the rule while the walk stands at the rule itself
5325    /// (see its arm). Every other block leaves the walk at its last row's end
5326    /// or past it — past a closing fence, say, which the counts below reach
5327    /// by other means.
5328    fn last_line_end(&self) -> Option<usize> {
5329        self.rows.last().map(|r| r.end_src.min(self.last_off))
5330    }
5331
5332    /// The quote's own trailing marker lines: the `>` / `> ` lines that lie past
5333    /// its last child but inside its span, one gutter row each.
5334    ///
5335    /// Pressing Enter at the end of `> a` writes `> a\n>\n> \n` — twig's
5336    /// spelling, and the right one. Those last two lines hold no block (a
5337    /// `block_quote`'s `content_span` still stops at its last child) so the
5338    /// children walk never reaches them, and they used to fall all the way to
5339    /// the document-level [`Builder::emit_trailing_blank_lines`], which knows no
5340    /// prefix: the gutter simply stopped, and a writer adding a line to a quote
5341    /// watched it draw as plain prose.
5342    ///
5343    /// This is only answerable since twig 3.2.0, where a Markdown `block_quote`'s
5344    /// span covers its own trailing marker lines (it reported `0..3` for that
5345    /// source and now reports `0..8`). Before that the lines belonged to no node
5346    /// at any level, and the only way to draw them was to sniff `>` off the raw
5347    /// source and re-derive the nesting depth by counting markers — format
5348    /// inference this crate exists to keep out of the render path.
5349    ///
5350    /// Each row is a real caret home rather than a decoration gap: the writer
5351    /// spelled every one of these lines with a marker of its own, so each is a
5352    /// line of the quote to stand on, not the spacing between two blocks (which
5353    /// is [`Builder::emit_separators_before`]'s, and falls *between* children
5354    /// where this never looks).
5355    fn emit_quote_trailing_lines(&mut self, pc: &[Glyph], end: usize) {
5356        let end = end.min(self.source.len());
5357        // From where the walk stands, not where the last row ends: a fenced
5358        // code block's last row is its last line of code, and its closing
5359        // fence (`> ```) is markup the block already stepped `last_off` past.
5360        // Counted from the row, the fence's line drew as an empty quoted line.
5361        let mut at = self.last_line_end().unwrap_or(0).max(self.last_off);
5362        // Walk line by line from the last child's end to the quote's, taking each
5363        // line's *end* as the row's offset — the caret home at the end of a line
5364        // is where one on an empty quoted line belongs, and it keeps every row's
5365        // offset distinct from its neighbours'.
5366        while at < end {
5367            let Some(k) = self.source[at..end].find('\n') else {
5368                break;
5369            };
5370            let line_start = at + k + 1;
5371            let line_end = self.source[line_start..end]
5372                .find('\n')
5373                .map_or(end, |i| line_start + i);
5374            self.push_row_at(pc.to_vec(), line_end);
5375            at = line_end;
5376        }
5377    }
5378
5379    /// The source offset the caret rests at on the blank line separating a block
5380    /// that ends at `prev_end` from the next block starting at `next_start`:
5381    /// just past the newline that terminates the previous block, but kept
5382    /// strictly before the next block so the offset is unique to this row.
5383    fn blank_line_offset(&self, prev_end: usize, next_start: usize) -> usize {
5384        let after_nl = self.source[prev_end..]
5385            .find('\n')
5386            .map_or(prev_end, |p| prev_end + p + 1);
5387        after_nl.min(next_start.saturating_sub(1)).max(prev_end)
5388    }
5389
5390    /// The source offset of each blank row between a block ending at `prev_end`
5391    /// and content starting at `next_start` — one per blank source line. The
5392    /// first newline terminates the previous block's line; every line it opens up
5393    /// to (but not including) the line that holds `next_start` is a blank row the
5394    /// caret can occupy. Offsets are unique and ascending so `pos_of_offset`
5395    /// resolves each to its own row. Empty when the two blocks are tight (no
5396    /// blank line between them).
5397    fn blank_rows_between(&self, prev_end: usize, next_start: usize) -> Vec<usize> {
5398        // Spans aren't always in tidy source order (e.g. a block after
5399        // frontmatter can start *before* the previous block's rendered content
5400        // ends). There's no blank line to place then — fall back to the clamped
5401        // single separator (an empty return) rather than slicing an inverted
5402        // range.
5403        if next_start <= prev_end {
5404            return Vec::new();
5405        }
5406        let gap = &self.source[prev_end..next_start];
5407        let Some(nl) = gap.find('\n') else {
5408            return Vec::new();
5409        };
5410        // The line holding `next_start` belongs to the next block; blank rows
5411        // stop before it.
5412        let next_line_start = self.source[..next_start].rfind('\n').map_or(0, |p| p + 1);
5413        let mut offs = Vec::new();
5414        let mut start = prev_end + nl + 1;
5415        while start < next_line_start {
5416            offs.push(start);
5417            match self.source[start..next_start].find('\n') {
5418                Some(k) => start += k + 1,
5419                None => break,
5420            }
5421        }
5422        offs
5423    }
5424
5425    /// Blank lines the user typed past the end of the last block (e.g. two
5426    /// `Enter`s to open a fresh paragraph) leave no AST node, so nothing renders
5427    /// and the caret appears stuck on the old line. Reconstruct one empty row
5428    /// per extra trailing newline from the source, each at its own offset, so
5429    /// the caret rides down onto the new line the moment it's created.
5430    ///
5431    /// `above` is the class of the last block in the document — the one this gap
5432    /// closes. A document with no blocks at all has nothing above these rows, and
5433    /// [`BlockClass::Paragraph`] is the honest answer there too: what they are is
5434    /// empty paragraphs, on both sides of the gap.
5435    fn emit_trailing_blank_lines(&mut self, above: BlockClass, hidden_end: usize) {
5436        // With no rows at all the count starts past any hidden frontmatter, not
5437        // at 0: its newlines are not trailing blank lines, and counting them
5438        // opened phantom rows *inside* the metadata for a frontmatter-only file.
5439        //
5440        // Or past the last hidden block, if that is later: a closing comment
5441        // draws no row, and its lines are not blank lines the author opened.
5442        //
5443        // Or past the last byte of content, if *that* is later: a fenced code
5444        // block's last row ends at its last line of code, and the closing
5445        // fence under it is markup with no row of its own — so counted from
5446        // the row, the fence's own line and terminator read as two blank lines
5447        // and opened a phantom empty paragraph whose offset was *inside* the
5448        // fence. Typing on it broke the fence. (A setext heading's underline
5449        // was the same shape.) Trailing whitespace is not content, so a line
5450        // of spaces still counts as the blank line it looks like.
5451        let last_end = self
5452            .last_line_end()
5453            .unwrap_or(hidden_end)
5454            .max(self.stepped_over)
5455            .max(self.source.trim_end().len());
5456        if last_end >= self.source.len() {
5457            return;
5458        }
5459        // The first newline after the last content just terminates that line, so
5460        // a lone trailing `\n` (an ordinary file ending) opens no blank row. A
5461        // *second* newline opens an empty paragraph: render it the way a block
5462        // boundary is rendered — a blank spacer row, then the empty paragraph row
5463        // the caret rests on — so the just-pressed-Enter view already shows the
5464        // gap it will keep once text is typed, and typing doesn't shift the line
5465        // down. One row per trailing newline (each its own caret offset), the
5466        // last landing at the document end where the caret sits.
5467        let extra = self.source[last_end..].matches('\n').count();
5468        if extra < 2 {
5469            return;
5470        }
5471        for k in 1..=extra {
5472            self.rows.push(VRow {
5473                glyphs: Vec::new(),
5474                end_src: last_end + k,
5475                // As between two blocks: the first blank row is the gap that
5476                // closes the block above, not somewhere to type. Nothing follows
5477                // to need a gap of its own, though, so every row after it is a
5478                // real empty paragraph — the end of the document bounds the last
5479                // one the way a following block would. Preserve flow makes even
5480                // that first row navigable, as it does every blank line.
5481                decoration: !self.preserve_soft && k == 1,
5482                code: false,
5483                code_lang: None,
5484                directive: false,
5485                directive_label: None,
5486                media: None,
5487                task: None,
5488                leaf_directive: None,
5489                heading: None,
5490                align: None,
5491                line_height: None,
5492                // The one drawn row here is a block boundary like any other —
5493                // "rendered the way a block boundary is rendered" is the whole
5494                // point of it — so it says so, and a frontend spacing boundaries
5495                // spaces this one the same. The rows below it are navigable empty
5496                // paragraphs, not gaps.
5497                boundary: (!self.preserve_soft && k == 1).then_some(Boundary {
5498                    above,
5499                    below: BlockClass::Paragraph,
5500                }),
5501                mark_ends: Vec::new(),
5502                math: Vec::new(),
5503            });
5504        }
5505    }
5506}
5507
5508// ── display width ────────────────────────────────────────────────────────────
5509//
5510// Two things a row can be counted in, and they are not the same number:
5511//
5512//   *glyphs*, one per codepoint — how the text is stored here, and what an
5513//   index into `VRow::glyphs` means; and
5514//   *columns*, one per terminal cell — where the text is drawn, and what every
5515//   `col` in this crate means.
5516//
5517// `你` is one glyph in two columns. Counting columns with `glyphs.len()` (or,
5518// in the source view, `chars().count()`) is the same number only for the ASCII
5519// that most fixtures are written in, and drifts one cell per wide character
5520// everywhere else — the caret drawn a column short of the text it types into.
5521// Everything below converts between the two; nothing else should have to.
5522
5523/// The display width of `s` in terminal cells.
5524///
5525/// Measured per grapheme cluster, because that is the unit a surface advances
5526/// by: `👨‍👩‍👧` is five codepoints measuring 2 + 0 + 2 + 0 + 2 cells one at a
5527/// time, but the character they spell is drawn in 2. Both frontends already
5528/// measure it that way — ratatui asks `unicode-width` per cluster, and the GUI
5529/// asks its own text system — so the caret only lands where the text is if this
5530/// agrees with them.
5531pub fn text_width(s: &str) -> usize {
5532    UnicodeWidthStr::width(s)
5533}
5534
5535/// One grapheme cluster of a laid-out row: the glyphs that spell it, and the
5536/// cells it is drawn in.
5537///
5538/// The cluster, not the glyph, is what has a width. A row's glyphs are one per
5539/// codepoint, so an accented letter or an emoji is several of them drawn in one
5540/// character's worth of cells — the glyph that opens the cluster claims those
5541/// cells, and the ones continuing it are drawn *inside* them rather than beside
5542/// them. It's the same cluster the stop table is built on: the opening glyph is
5543/// the one a caret can rest on, and so the only one whose column it can be
5544/// drawn at.
5545struct Cluster {
5546    /// Index of the glyph that opens it.
5547    glyph: usize,
5548    /// The display column it starts at.
5549    col: usize,
5550    /// How many cells it is drawn in. Zero for a cluster with no width of its
5551    /// own (a lone joiner), which therefore sits at no column at all.
5552    cells: usize,
5553}
5554
5555/// Walk a row's glyphs as the clusters they spell, in column order.
5556fn clusters(glyphs: &[Glyph]) -> Vec<Cluster> {
5557    let text: String = glyphs.iter().map(|g| g.ch).collect();
5558    let mut out = Vec::new();
5559    let (mut glyph, mut col) = (0, 0);
5560    for cluster in text.graphemes(true) {
5561        let cells = text_width(cluster);
5562        out.push(Cluster { glyph, col, cells });
5563        // One glyph per codepoint, so a cluster spans exactly its own.
5564        glyph += cluster.chars().count();
5565        col += cells;
5566    }
5567    out
5568}
5569
5570/// The display width of a run of glyphs.
5571fn glyphs_width(glyphs: &[Glyph]) -> usize {
5572    clusters(glyphs).last().map_or(0, |c| c.col + c.cells)
5573}
5574
5575/// A cell's display width — the widest of its lines, since an in-cell `\n` break
5576/// splits it into several. Sizes the column that must hold every line.
5577fn cell_width(glyphs: &[Glyph]) -> usize {
5578    glyphs
5579        .split(|g| g.ch == '\n')
5580        .map(glyphs_width)
5581        .max()
5582        .unwrap_or(0)
5583}
5584
5585/// Whether a raw inline HTML tag is a line break (`<br>`, `<br/>`, `<br />`,
5586/// case-insensitively) — the one tag a table cell reads as an in-cell break.
5587fn is_br(text: Option<&str>) -> bool {
5588    let Some(t) = text else { return false };
5589    matches!(
5590        t.trim().to_ascii_lowercase().replace(' ', "").as_str(),
5591        "<br>" | "<br/>"
5592    )
5593}
5594
5595impl VRow {
5596    /// The row's width in display columns — and so the column of the caret
5597    /// placed past its last glyph, which is the rightmost column it can occupy.
5598    fn width(&self) -> usize {
5599        glyphs_width(&self.glyphs)
5600    }
5601
5602    /// The display column glyph `i` is drawn at. Glyphs continuing a cluster
5603    /// report the column of the glyph that opened it, since that is where they
5604    /// are drawn; none of them is ever a stop, so no caret is placed by it.
5605    fn col_of_glyph(&self, i: usize) -> usize {
5606        clusters(&self.glyphs)
5607            .iter()
5608            .rev()
5609            .find(|c| c.glyph <= i)
5610            .map_or(0, |c| c.col)
5611    }
5612
5613    /// The glyph drawn at display column `col`, or `None` past the row's last
5614    /// cell.
5615    ///
5616    /// A column landing on the *second* cell of a wide glyph resolves to that
5617    /// glyph: half a character is not a place to be, so clicking either cell of
5618    /// `你` means `你`, and the caret comes to rest at its start — the column it
5619    /// would be drawn at anyway. That rule is what makes the mapping invertible:
5620    /// every offset has one column, and every column has one offset.
5621    fn glyph_at_col(&self, col: usize) -> Option<usize> {
5622        clusters(&self.glyphs)
5623            .into_iter()
5624            .find(|c| col < c.col + c.cells)
5625            .map(|c| c.glyph)
5626    }
5627}
5628
5629// ── helpers ──────────────────────────────────────────────────────────────────
5630
5631/// The caret home inside an *empty* table cell (`col`, 0-based) whose node
5632/// `span` is `src` starting at byte `start`. twig gives an empty cell no
5633/// `content_span`, so its interior is read from the pipes: the home is one
5634/// space past the pipe that opens the cell — mimicking the `| ` padding a
5635/// filled cell has — and never at or past the pipe that closes it. So
5636/// `|  |  |` gives the two cells distinct, editable homes instead of both
5637/// collapsing onto the row's start.
5638///
5639/// Two shapes of span are read. A twig from 3.3.3 gave every cell of a row
5640/// the *row's* span, so the cell's own pipes are the `col`-th and
5641/// `col+1`-th unescaped `|` in it; a later twig spans a cell from the pipe
5642/// that opens it to the one that closes it, exclusive, so the span holds at
5643/// most that one pipe, at its start, and the closing one is the byte past
5644/// its end. The two are told apart by the pipes the span holds — a row's
5645/// span has several, or one that is not at its start.
5646fn empty_cell_offset(src: &str, start: usize, col: usize) -> usize {
5647    let bytes = src.as_bytes();
5648    let mut pipes = Vec::new();
5649    for (i, &b) in bytes.iter().enumerate() {
5650        if b == b'|' && (i == 0 || bytes[i - 1] != b'\\') {
5651            pipes.push(i);
5652        }
5653    }
5654    let whole_row = pipes.len() > 1 || pipes.first().is_some_and(|&i| i != 0);
5655    let (open, close) = if whole_row {
5656        (pipes.get(col).copied(), pipes.get(col + 1).copied())
5657    } else {
5658        (pipes.first().copied(), Some(src.len()))
5659    };
5660    match (open, close) {
5661        (Some(open), Some(close)) => {
5662            let lo = open + 1; // just inside the opening pipe
5663            let hi = close.saturating_sub(1); // just inside the closing pipe
5664            let inside = if hi < lo {
5665                lo
5666            } else {
5667                (open + 2).clamp(lo, hi)
5668            };
5669            start + inside
5670        }
5671        (Some(open), None) => start + open + 1,
5672        _ => start,
5673    }
5674}
5675
5676/// One laid-out table cell: its rendered text, the source range that text
5677/// occupies (`start`/`end` are the caret anchors decoration points at), and the
5678/// column alignment its padding honours.
5679///
5680/// `glyphs` is the cell's inline content *unwrapped* — the box-drawn rows wrap
5681/// it to a column width, but a frontend laying the grid out itself needs the
5682/// text before that decision was made.
5683#[derive(Clone)]
5684pub struct TableCell {
5685    pub glyphs: Vec<Glyph>,
5686    pub start: usize,
5687    pub end: usize,
5688    pub align: Alignment,
5689}
5690
5691/// One row of a table's grid, as the document spells it — not as it's drawn.
5692#[derive(Clone)]
5693pub struct TableRow {
5694    /// A header row: drawn bold, and ruled off from the body below it.
5695    pub head: bool,
5696    pub cells: Vec<TableCell>,
5697}
5698
5699/// A table's structure, published alongside the box-drawn rows that spell it.
5700///
5701/// The rows in [`VisualMap::rows`] are the *default monospace* picture of a
5702/// table: every border a `│`, every column a whole number of character cells.
5703/// That picture is exactly right on any monospace surface, and unfixable off one
5704/// — in a proportional font the `│`s of two rows land at different x and the grid
5705/// shears. So a frontend that draws its own geometry reads this instead: the
5706/// cells, their alignment, and which rows are the head, with no opinion about
5707/// how wide a column is or what a border looks like.
5708///
5709/// Both are always built. The TUI paints `rows` and ignores this; the GUI skips
5710/// `rows` for the span in `rows_span` and draws from here. They describe the
5711/// same cells, so the caret lands on the same offsets either way.
5712#[derive(Clone)]
5713pub struct TableInfo {
5714    /// The `VisualMap::rows` this table's picture occupies, borders included —
5715    /// what a frontend drawing its own table skips over.
5716    pub rows_span: Range<usize>,
5717    /// The end of the table node's source span, and the offset its trailing
5718    /// caret stop sits at — the one caret home past the last cell, held by the
5719    /// bottom border row's end. Typing there opens a paragraph under the table
5720    /// rather than joining the block; see `Doc::open_paragraph_at_block_edge`.
5721    pub end_src: usize,
5722    /// The block prefix every row of this table carries — a blockquote's `│ `
5723    /// gutter, a list item's indent. Empty for a table at the top level.
5724    ///
5725    /// A frontend drawing its own grid has to render this and start the table
5726    /// past it, exactly as the picture does; a table nested in a quote that
5727    /// draws flush at the left margin has left the quote.
5728    pub prefix: Vec<Glyph>,
5729    pub grid: Vec<TableRow>,
5730}
5731
5732/// A fenced or indented code block, named by the [`VisualMap::rows`] it occupies.
5733///
5734/// Unlike a table, the rows *are* the block's content — a frontend still paints
5735/// them, it just draws a border and a tinted background around the whole span
5736/// and lets the code inside scroll horizontally instead of wrapping. So this
5737/// carries only the row range; there's no structural alternative to the picture
5738/// the way [`TableInfo`] is one. Derived from [`VRow::code`] — see
5739/// [`code_block_spans`].
5740#[derive(Clone, Debug, PartialEq, Eq)]
5741pub struct CodeBlockInfo {
5742    /// The contiguous run of [`VisualMap::rows`] this code block spans, blank
5743    /// code lines included.
5744    pub rows_span: Range<usize>,
5745    /// The block's language, from a fenced block's info string — what a frontend
5746    /// paints as a small label on the box (`` ```rust `` → `Some("rust")`).
5747    /// `None` for a fence written without one, or an indented block. Editing it
5748    /// goes through [`crate::Doc::set_code_language`], which re-finds the fence
5749    /// in the AST, so this stays a display string.
5750    pub lang: Option<String>,
5751}
5752
5753/// A block-level image (`![alt](url)` on its own line), named by the single
5754/// [`VisualMap::rows`] row it occupies.
5755///
5756/// Like [`CodeBlockInfo`], the row *is* the block's default rendering — a plain
5757/// surface paints the `🖼 alt` placeholder glyphs as-is. An image-capable
5758/// frontend instead **skips the row in `rows_span`** and paints the resolved
5759/// picture there, exactly as it skips a [`TableInfo`]'s box-drawn rows. Derived
5760/// from [`VRow::media`] by [`media_spans`], so it survives the row reuse of
5761/// [`BlockCache`] and [`build_spliced`].
5762#[derive(Clone, Debug, PartialEq, Eq)]
5763pub struct MediaInfo {
5764    /// The [`VisualMap::rows`] rows this media's placeholder occupies — what a
5765    /// capable frontend replaces with the picture or player.
5766    pub rows_span: Range<usize>,
5767    /// Whether this is a picture, a movie, or a sound — which widget the
5768    /// frontend builds over [`rows_span`](MediaInfo::rows_span). A frontend that
5769    /// handles only some kinds leaves the rest as core's placeholder rows, which
5770    /// already read sensibly on their own.
5771    pub kind: MediaKind,
5772    /// The media's link destination — a path, URL, or `data:` URI, verbatim from
5773    /// the AST. A frontend resolves a relative path against the document's own
5774    /// directory; core does no I/O. For a `<picture>` this is the `<img>`
5775    /// fallback — the source used when no [`sources`](MediaInfo::sources) media
5776    /// query matches (or the frontend has no theme). Empty when a `<video>`/
5777    /// `<audio>` carries no `src` and names its candidates in `<source>`s
5778    /// instead; [`resolve`](MediaInfo::resolve) already accounts for that.
5779    pub destination: String,
5780    /// The `<source>` alternatives in document order, or empty for a plain
5781    /// image. See [`MediaSource`]; a theme- or codec-aware frontend picks one and
5782    /// otherwise loads [`destination`](MediaInfo::destination).
5783    pub sources: Vec<MediaSource>,
5784    /// The media's alt text, flattened from its inline children (empty when it
5785    /// has none).
5786    pub alt: String,
5787    /// A `<video poster="…">`'s still frame, or empty when there is none — an
5788    /// image destination, resolved exactly as [`destination`] is.
5789    ///
5790    /// [`destination`]: MediaInfo::destination
5791    pub poster: String,
5792}
5793
5794/// One leaf directive (`::name{…}`) as a frontend sees it: which rows its
5795/// placeholder occupies, its type, and its attributes. A plain surface paints
5796/// the `⧉ name` placeholder glyphs as-is; a frontend that knows the host app's
5797/// vocabulary **skips the rows in `rows_span`** and paints the real thing there,
5798/// exactly as an image-capable one does with [`MediaInfo`]. Derived from
5799/// [`VRow::leaf_directive`] by [`directive_spans`].
5800///
5801/// Core resolves nothing here — it has no idea what an `embed` or a `toc` is,
5802/// and deliberately so: the directive vocabulary belongs to the app on top.
5803#[derive(Clone, Debug, PartialEq, Eq)]
5804pub struct DirectiveInfo {
5805    /// The [`VisualMap::rows`] rows this directive's placeholder occupies — the
5806    /// label row plus any blank fillers under it.
5807    pub rows_span: Range<usize>,
5808    /// The directive's type (`embed`, `toc`, `vis`), no leading colons.
5809    pub name: String,
5810    /// Its `{…}` attributes in source order; a bare one has a `None` value.
5811    pub attrs: Vec<(String, Option<String>)>,
5812    /// Its `[label]` text, flattened from its inline children (empty when it has
5813    /// none) — what the placeholder row shows.
5814    pub label: String,
5815}
5816
5817/// One formula as a frontend sees it: where its picture goes, and the TeX to
5818/// typeset for it. Two shapes, told apart by [`glyph`](MathInfo::glyph):
5819///
5820/// - An **inline atom** — `$E = mc^2$` in a line of prose — is one
5821///   [`Role::Math`] glyph on `row` at index `glyph`, standing for the whole
5822///   formula. A frontend that paints pictures in a line typesets the TeX at
5823///   the run's font size and draws the picture in the glyph's place, as wide
5824///   as the picture is and with the text baseline through it at its height —
5825///   a run delegate on Apple, an inline element on the web. The glyph is a
5826///   caret stop at the formula's start; the stop after it is the next glyph's.
5827/// - A **display block** — a paragraph holding nothing but `$$…$$` — is the
5828///   placeholder row (`∑ tex`, every glyph at the formula's start) plus the
5829///   blank fillers `rows_span` reserves under it, exactly a [`MediaInfo`]'s
5830///   shape. A frontend skips those rows and paints the picture there, centred
5831///   on the measure.
5832///
5833/// Either way the frontend supplies the *width*: core lays out in glyphs and
5834/// cannot know how wide a typeset formula is, which is why an inline atom is
5835/// one glyph and not a run of them. Only in a **column-wrapped** build does
5836/// that matter to the wrap: a terminal never asks for atoms (see
5837/// [`Surface::inline_pictures`]), and the web re-fits a row the picture
5838/// overflows.
5839///
5840/// A plain surface paints the glyphs as they are — `∑` for an atom, the
5841/// labelled row for a block — and needs none of this. Derived from
5842/// [`VRow::math`] by [`math_spans`], so it survives the row reuse of
5843/// [`BlockCache`] and [`build_spliced`].
5844#[derive(Clone, Debug, PartialEq, Eq)]
5845pub struct MathInfo {
5846    /// The [`VisualMap::rows`] rows this formula occupies: `row..row + 1` for
5847    /// an atom, the placeholder row and its fillers for a block.
5848    pub rows_span: Range<usize>,
5849    /// The row the atom glyph, or the block's placeholder label, is on.
5850    pub row: usize,
5851    /// The atom's index into that row's glyphs, or `None` for a block.
5852    pub glyph: Option<usize>,
5853    /// The TeX between the delimiters, verbatim.
5854    pub tex: String,
5855    /// Display style rather than text style — see [`MathMark::display`].
5856    pub display: bool,
5857    /// The formula's source start: where a click on its picture lands the
5858    /// caret, and the same offset every placeholder glyph carries.
5859    pub src: usize,
5860}
5861
5862impl DirectiveInfo {
5863    /// The value of attribute `key`, if it has one with a value. The convenience
5864    /// a frontend reaches for first (`info.attr("src")`), since almost every
5865    /// directive that draws as something real is pointed at by one attribute.
5866    pub fn attr(&self, key: &str) -> Option<&str> {
5867        self.attrs
5868            .iter()
5869            .find(|(k, _)| k == key)
5870            .and_then(|(_, v)| v.as_deref())
5871    }
5872}
5873
5874impl MediaInfo {
5875    /// The image URL to load under `scheme`: the first [`sources`] `<source>`
5876    /// whose media query matches, else the [`destination`] `<img>` fallback. The
5877    /// pick is a `<source>`'s first `srcset` URL or the destination — a frontend
5878    /// resolves whichever it gets against the document directory exactly as it
5879    /// resolves `destination`, and reserves/keys the picture under `destination`
5880    /// regardless, so a theme switch just re-picks without disturbing the layout.
5881    ///
5882    /// Only `prefers-color-scheme` is understood (that's what a light/dark banner
5883    /// uses); a `<source>` with any other media query is skipped, and one with no
5884    /// media at all always matches (an unconditional override). With no matching
5885    /// source — including every frontend that can't/doesn't theme and passes
5886    /// [`ColorScheme::Light`] to a dark-only picture — it's the plain `<img>`.
5887    ///
5888    /// [`sources`]: MediaInfo::sources
5889    /// [`destination`]: MediaInfo::destination
5890    pub fn resolve(&self, scheme: ColorScheme) -> &str {
5891        if let Some(url) = self
5892            .sources
5893            .iter()
5894            .find(|s| media_matches(&s.media, scheme))
5895            .and_then(|s| first_srcset_url(&s.srcset))
5896        {
5897            return url;
5898        }
5899        // A `<video>`/`<audio>` may carry no `src` of its own, naming its
5900        // candidates only in child `<source>`s — none of which matched above,
5901        // because a codec-typed `<source>` has no media query and core judges no
5902        // MIME types. Falling through to an empty destination would hand the
5903        // frontend nothing to load, so take the first candidate URL instead and
5904        // let the frontend reject it if it can't decode it. An `<img>` never
5905        // reaches this: its `src` is the picture.
5906        if self.destination.is_empty()
5907            && let Some(url) = self
5908                .sources
5909                .iter()
5910                .find_map(|s| first_srcset_url(&s.srcset))
5911        {
5912            return url;
5913        }
5914        &self.destination
5915    }
5916
5917    /// The **still picture** that stands for this media under `scheme`, for a
5918    /// frontend that can rasterize an image but not play a movie — a terminal, or
5919    /// a GUI still growing its player. `None` when there is no picture to draw,
5920    /// which is the honest answer for audio and for a poster-less video: the
5921    /// caller leaves core's labelled placeholder row, which already reads as
5922    /// *a thing that isn't text*.
5923    ///
5924    /// This exists so those frontends never hand a `.mp4` to an image decoder.
5925    /// That fails harmlessly today (a failed decode falls back to the same
5926    /// placeholder), but it spends a file read and a decode attempt per frame to
5927    /// arrive where this gets in one match.
5928    pub fn still(&self, scheme: ColorScheme) -> Option<&str> {
5929        match self.kind {
5930            MediaKind::Image => Some(self.resolve(scheme)),
5931            // A `poster` is an image destination, so it resolves the same way —
5932            // but it is named directly and has no `<source>` alternatives of its
5933            // own, so it needs no theme matching.
5934            MediaKind::Video if !self.poster.is_empty() => Some(&self.poster),
5935            MediaKind::Video | MediaKind::Audio => None,
5936        }
5937    }
5938}
5939
5940/// A frontend's active color scheme — what a `<picture>`'s `prefers-color-scheme`
5941/// `<source>`s are matched against by [`MediaInfo::resolve`]. A frontend with no
5942/// notion of theme passes [`Light`](ColorScheme::Light), the web's own default.
5943#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5944pub enum ColorScheme {
5945    Light,
5946    Dark,
5947}
5948
5949/// Whether a `<source media="…">` query applies under `scheme`. Empty media is
5950/// an unconditional `<source>` (always matches); otherwise only a
5951/// `prefers-color-scheme: dark|light` feature is understood — anything else
5952/// (a width query, `print`, …) doesn't match, so resolution falls through to the
5953/// next source or the `<img>`. Deliberately lax about the surrounding syntax
5954/// (`(prefers-color-scheme: dark)`, `screen and (prefers-color-scheme:dark)`):
5955/// it keys off the feature and its value, which is all the theme case needs.
5956fn media_matches(media: &str, scheme: ColorScheme) -> bool {
5957    let media = media.trim();
5958    if media.is_empty() {
5959        return true;
5960    }
5961    let lower = media.to_ascii_lowercase();
5962    let Some(after) = lower
5963        .split_once("prefers-color-scheme")
5964        .map(|(_, rest)| rest)
5965    else {
5966        return false;
5967    };
5968    // Skip the `:` and any spaces to reach the value word.
5969    let value = after.trim_start_matches([':', ' ', '\t']);
5970    let wanted = match scheme {
5971        ColorScheme::Light => "light",
5972        ColorScheme::Dark => "dark",
5973    };
5974    value.starts_with(wanted)
5975}
5976
5977/// The first URL in a `srcset`: its first comma-separated candidate, before any
5978/// `1x`/`2x`/width descriptor. The theme case only ever puts one URL per
5979/// `<source>`, so the first candidate is the picture.
5980fn first_srcset_url(srcset: &str) -> Option<&str> {
5981    let first = srcset.split(',').next()?.trim();
5982    first.split_whitespace().next().filter(|u| !u.is_empty())
5983}
5984
5985/// The narrowest a column may be squeezed. Below a few characters a column
5986/// stops carrying text and just shreds it one letter per line, which is worse
5987/// than letting the grid run wide.
5988const MIN_COL_WIDTH: usize = 3;
5989
5990/// Shrink `widths` until the grid fits `avail` screen columns, taking from the
5991/// widest column each time so the loss is shared out rather than falling on
5992/// whichever column happens to be last. No column goes below
5993/// [`MIN_COL_WIDTH`]; a table with more columns than the surface has room for
5994/// still overflows, which is the honest outcome — there's nothing left to give.
5995fn fit_widths(widths: &mut [usize], avail: usize) {
5996    // Chrome: each column is its content plus a gutter either side, and every
5997    // column is closed by a `│` — with one more opening the row.
5998    let budget = avail.saturating_sub(3 * widths.len() + 1);
5999    while widths.iter().sum::<usize>() > budget {
6000        let Some(w) = widths.iter_mut().filter(|w| **w > MIN_COL_WIDTH).max() else {
6001            return;
6002        };
6003        *w -= 1;
6004    }
6005}
6006
6007/// Word-wrap `glyphs` into lines of at most `width` columns, hard-breaking any
6008/// single word too long to fit.
6009///
6010/// Unlike a paragraph — where an overlong word just trails off the end of the
6011/// line — a table column is a hard boundary: a glyph past it lands on top of
6012/// the border, or on the next cell. So the width here is a promise, and a word
6013/// that won't keep it is broken.
6014///
6015/// The space at a break is dropped rather than hung past the edge. Its offset
6016/// isn't lost: the caller gives every line an end stop just past its last
6017/// glyph, which is exactly where that space was.
6018///
6019/// `width` is in display columns, and a break only ever falls between grapheme
6020/// clusters. Both matter to more than the picture: the caller anchors each
6021/// line's end stop just past its last glyph, so a line cut mid-cluster would
6022/// put a caret stop inside a character — reachable by Down or a click, and the
6023/// next Backspace would take the cluster apart from the middle.
6024///
6025/// An explicit in-cell break (a `\n` glyph, from a `<br>`) is a hard boundary:
6026/// each run between the breaks wraps on its own and the results stack. The break
6027/// glyphs are dropped — the caller's per-line end stop already sits exactly where
6028/// each break was, so no offset is lost.
6029fn wrap_glyphs(glyphs: &[Glyph], width: usize) -> Vec<Vec<Glyph>> {
6030    if glyphs.iter().any(|g| g.ch == '\n') {
6031        return glyphs
6032            .split(|g| g.ch == '\n')
6033            .flat_map(|seg| wrap_segment(seg, width))
6034            .collect();
6035    }
6036    wrap_segment(glyphs, width)
6037}
6038
6039/// [`wrap_glyphs`] for a run with no explicit breaks — the word-wrap proper.
6040fn wrap_segment(glyphs: &[Glyph], width: usize) -> Vec<Vec<Glyph>> {
6041    let width = width.max(1);
6042    // Words are maximal non-space runs, each carrying the space that followed it
6043    // — which survives only if the next word joins it on this line.
6044    let mut words: Vec<(Vec<Glyph>, Option<Glyph>)> = Vec::new();
6045    let mut word: Vec<Glyph> = Vec::new();
6046    for g in glyphs {
6047        if g.ch == ' ' {
6048            words.push((std::mem::take(&mut word), Some(g.clone())));
6049        } else {
6050            word.push(g.clone());
6051        }
6052    }
6053    if !word.is_empty() {
6054        words.push((word, None));
6055    }
6056
6057    let mut lines: Vec<Vec<Glyph>> = Vec::new();
6058    let mut line: Vec<Glyph> = Vec::new();
6059    let mut used = 0usize;
6060    let mut gap: Option<Glyph> = None;
6061    for (word, space) in words {
6062        for chunk in hard_break(&word, width) {
6063            let sep = gap.is_some() as usize;
6064            let cells = glyphs_width(chunk);
6065            if !line.is_empty() && used + sep + cells > width {
6066                lines.push(std::mem::take(&mut line));
6067                used = 0;
6068                gap = None; // the break swallows the space
6069            }
6070            if let Some(sp) = gap.take() {
6071                line.push(sp);
6072                used += 1;
6073            }
6074            line.extend_from_slice(chunk);
6075            used += cells;
6076        }
6077        gap = space;
6078    }
6079    // An empty cell is still one (empty) line — it has an end the caret can
6080    // sit at, which is how you type into it.
6081    if !line.is_empty() || lines.is_empty() {
6082        lines.push(line);
6083    }
6084    lines
6085}
6086
6087/// Break a single word into pieces of at most `width` columns, cutting only
6088/// between grapheme clusters — the replacement for slicing it into fixed runs
6089/// of glyphs, which measures a wide character as one column and can cut an
6090/// emoji in half.
6091///
6092/// A cluster wider than the whole column still gets a piece to itself: there is
6093/// nowhere legal to cut it, and overflowing by a cell is better than splitting a
6094/// character. An empty word yields no pieces at all, which is what keeps a
6095/// double space from opening a line of its own.
6096fn hard_break(word: &[Glyph], width: usize) -> Vec<&[Glyph]> {
6097    let mut out = Vec::new();
6098    if word.is_empty() {
6099        return out;
6100    }
6101    let (mut start, mut used) = (0usize, 0usize);
6102    for c in clusters(word) {
6103        if used > 0 && used + c.cells > width {
6104            out.push(&word[start..c.glyph]);
6105            start = c.glyph;
6106            used = 0;
6107        }
6108        used += c.cells;
6109    }
6110    out.push(&word[start..]);
6111    out
6112}
6113
6114/// A table rule spanning `widths`, e.g. `┌──────┬─────┐`. Each column is its
6115/// content width plus the one-space gutter on either side.
6116fn rule_text(widths: &[usize], left: char, mid: char, right: char) -> String {
6117    let mut s = String::new();
6118    s.push(left);
6119    for (i, w) in widths.iter().enumerate() {
6120        if i > 0 {
6121            s.push(mid);
6122        }
6123        for _ in 0..w + 2 {
6124            s.push('─');
6125        }
6126    }
6127    s.push(right);
6128    s
6129}
6130
6131/// Push real document text: each glyph maps to its own source byte, and the one
6132/// that opens a grapheme cluster is the caret stop for the whole cluster.
6133///
6134/// Per cluster rather than per codepoint because a cluster is the character the
6135/// user sees, and it's the unit backspace and delete already step by. A stop
6136/// inside 👨‍👩‍👧 — five codepoints strung together with joiners — is a caret
6137/// parked in the middle of a character: one press of Right lands there, and the
6138/// next Backspace severs a joiner from what it joined, leaving a dangling ZWJ in
6139/// the source. The rest of the cluster still gets its glyph (it has to be
6140/// drawn); it just isn't somewhere to stand.
6141fn push_text(out: &mut Vec<Glyph>, text: &str, base_src: usize, style: Style) {
6142    for (gi, cluster) in text.grapheme_indices(true) {
6143        for (ci, ch) in cluster.char_indices() {
6144            out.push(Glyph {
6145                ch,
6146                style,
6147                src: base_src + gi + ci,
6148                stop: ci == 0,
6149            });
6150        }
6151    }
6152}
6153
6154/// [`push_text`] for one line of a highlighted code block: the same glyphs at
6155/// the same offsets, each additionally carrying the [`Token`] of the span it
6156/// falls in — `spans` being the line's entry from [`code_tokens`], ascending
6157/// byte ranges *into `text`*. A byte between spans keeps `style` as it is.
6158///
6159/// Offsets are what matters here: a token changes how a glyph is painted and
6160/// nothing about where it is or which source byte it stands on, so a caret
6161/// walks a highlighted block exactly as it walks an unhighlighted one.
6162fn push_code_text(
6163    out: &mut Vec<Glyph>,
6164    text: &str,
6165    base_src: usize,
6166    style: Style,
6167    spans: &[(Range<usize>, Token)],
6168) {
6169    let mut spans = spans.iter().peekable();
6170    for (gi, cluster) in text.grapheme_indices(true) {
6171        // Spans are ascending, so the one covering this cluster's first byte
6172        // is at or after the one that covered the last; step past those ended.
6173        while spans.peek().is_some_and(|(r, _)| r.end <= gi) {
6174            spans.next();
6175        }
6176        let token = spans
6177            .peek()
6178            .filter(|(r, _)| r.contains(&gi))
6179            .map(|(_, t)| *t);
6180        // A cluster is classed whole, by its first byte: a grammar that split
6181        // an emoji's scalars between two tokens would otherwise split the
6182        // glyph, and no grammar means to.
6183        let style = style.token(token);
6184        for (ci, ch) in cluster.char_indices() {
6185            out.push(Glyph {
6186                ch,
6187                style,
6188                src: base_src + gi + ci,
6189                stop: ci == 0,
6190            });
6191        }
6192    }
6193}
6194
6195/// One line's highlighting — `crate::syntax::LineTokens`, spelled here so the
6196/// shape exists whether or not the feature that fills it does.
6197type LineTokens = Vec<(Range<usize>, Token)>;
6198
6199/// The syntax highlighting for a code block's lines, or `None` when the fence's
6200/// language is not one the grammars know. Without the `syntax` feature nothing
6201/// is known, and every code glyph draws in the plain code colour.
6202#[cfg(feature = "syntax")]
6203fn code_tokens(lang: &str, lines: &[&str]) -> Option<Vec<LineTokens>> {
6204    crate::syntax::highlight(lang, lines)
6205}
6206
6207#[cfg(not(feature = "syntax"))]
6208fn code_tokens(_lang: &str, _lines: &[&str]) -> Option<Vec<LineTokens>> {
6209    None
6210}
6211
6212/// Emit an inline `str`/`smart_punctuation` run, mapping every visible char back
6213/// to its *true* source byte even when the source carries backslash escapes the
6214/// parsed `text` dropped (`\*` → `*`). The naive `span.start + text_offset`
6215/// mapping [`push_text`] uses drifts by one byte after each escape, so a caret or
6216/// click past an escaped `*` would land on the wrong character; walking the text
6217/// against its source keeps them aligned, and the hidden escape backslash gets no
6218/// glyph of its own (it is a spelling artefact, not something the caret lands on).
6219fn push_escaped_text(
6220    out: &mut Vec<Glyph>,
6221    text: &str,
6222    span: Range<usize>,
6223    source: &str,
6224    style: Style,
6225) {
6226    let end = span.end.min(source.len());
6227    let src = source.get(span.start..end).unwrap_or("");
6228    // Fast path — no dropped bytes, so text and source align 1:1 (the common
6229    // case: prose with no escapes). Byte lengths equal ⇒ no backslash was eaten.
6230    if src.len() == text.len() {
6231        push_text(out, text, span.start, style);
6232        return;
6233    }
6234    // Slow path: some `\` was consumed. Walk char-by-char, skipping a backslash
6235    // in the source exactly when it escapes the next visible char (a real escape),
6236    // never when it is a literal backslash the parse kept (that case has equal
6237    // lengths and takes the fast path above).
6238    let sb = src.as_bytes();
6239    let mut si = 0usize;
6240    'text: for (_, cluster) in text.grapheme_indices(true) {
6241        for (ci, ch) in cluster.char_indices() {
6242            // The text outlasted the source it is being mapped onto. In a
6243            // consistent document that cannot happen on this path: the slow path
6244            // is only entered when the two lengths differ, and everything that
6245            // makes them differ makes the *source* the longer one — an escape
6246            // backslash the parse ate, or source folded into a neighbouring node.
6247            // A `smart_punctuation` node reports its canonical ASCII spelling
6248            // (`--`, `...`, `"`), which is never longer than what was written.
6249            //
6250            // So reaching here means `span` was measured against a document that
6251            // `source` is no longer, and there is no honest offset left to give
6252            // the remaining characters. Stop: the row comes out short, which is
6253            // a wrong picture of a document that is already inconsistent. The
6254            // alternative was `si` stepping past the end and the slice below
6255            // panicking — which is what it did, in a paint loop.
6256            if si >= sb.len() {
6257                break 'text;
6258            }
6259            // Advance to the source character this one came from, stepping over
6260            // whatever the parse dropped on the way. An escape backslash is the
6261            // common case, but not the only one: a span can cover source that
6262            // was folded into a neighbouring node (smart punctuation next to a
6263            // bracket gives `text: "]"` over a source span of `"…]"`). Advancing
6264            // by the *text* character's length assumed escapes were the only
6265            // divergence, so one dropped multi-byte character desynchronized
6266            // every glyph after it — placing `]` inside the `…` before it.
6267            while si < sb.len() && !src[si..].starts_with(ch) {
6268                si += src[si..].chars().next().map_or(1, char::len_utf8);
6269            }
6270            out.push(Glyph {
6271                ch,
6272                style,
6273                src: span.start + si.min(src.len()),
6274                stop: ci == 0,
6275            });
6276            si += src[si..]
6277                .chars()
6278                .next()
6279                .map_or(ch.len_utf8(), char::len_utf8);
6280        }
6281    }
6282}
6283
6284/// Build synthetic decoration glyphs (a bullet, a gutter) all pointing at `src`,
6285/// each carrying `role` so the frontend can style it (`Role::Body` for plain
6286/// padding). Synthetic glyphs are never caret stops — they share one offset, so
6287/// the caret steps over them (a click still lands at `src`).
6288fn synth(text: &str, role: Role, src: usize) -> Vec<Glyph> {
6289    let style = Style::default().role(role);
6290    text.chars()
6291        .map(|ch| Glyph {
6292            ch,
6293            style,
6294            src,
6295            stop: false,
6296        })
6297        .collect()
6298}
6299
6300fn concat(a: &[Glyph], b: &[Glyph]) -> Vec<Glyph> {
6301    let mut v = a.to_vec();
6302    v.extend_from_slice(b);
6303    v
6304}
6305
6306/// The columns a row's prefix (a bullet, a quote gutter, an indent) takes up
6307/// before the text it introduces — what the wrap budget has left to spend.
6308fn prefix_width(prefix: &[Glyph]) -> usize {
6309    glyphs_width(prefix)
6310}
6311
6312/// The label shown for an image with no alt text: the final path segment of its
6313/// destination (`img/cat.png` → `cat.png`), the whole destination when it has no
6314/// separator, and `"image"` when it's empty. A `data:` URI (which has no useful
6315/// tail) shows its scheme so the placeholder isn't a wall of base64.
6316fn media_label(dest: &str) -> String {
6317    if dest.is_empty() {
6318        return "image".to_string();
6319    }
6320    if dest.starts_with("data:") {
6321        return "data:…".to_string();
6322    }
6323    // Trim a query/fragment so a URL's `?v=2#frag` doesn't ride along.
6324    let clean = dest.split(['?', '#']).next().unwrap_or(dest);
6325    let tail = clean
6326        .trim_end_matches('/')
6327        .rsplit(['/', '\\'])
6328        .next()
6329        .unwrap_or(clean);
6330    if tail.is_empty() {
6331        dest.to_string()
6332    } else {
6333        tail.to_string()
6334    }
6335}
6336
6337/// A directive's attributes read as a human label — what a frontend puts on a
6338/// container's tinted panel, and what an attribute-bearing inline directive
6339/// shows in its chip.
6340///
6341/// Reads BOTH conventions diaryx content actually uses: twig's own dot-prefixed
6342/// classes (`{.public .family}`, arriving as one combined `class` attr) and bare
6343/// pandoc-style words with no leading dot (`{public family}` — what
6344/// `diaryx_core::visibility`'s publish-time filter and apps/web's directive
6345/// serializer both write, and which twig parses as one valueless attribute
6346/// each). Reading only `.class` would leave every *existing* diaryx `:::vis{…}`
6347/// block unlabeled. A `key=value` attr is configuration rather than a name, so
6348/// it contributes nothing. `None` when nothing readable is left.
6349fn directive_attr_label(attrs: &[(String, Option<String>)]) -> Option<String> {
6350    let mut parts: Vec<String> = Vec::new();
6351    for (k, v) in attrs {
6352        if k == "class" {
6353            if let Some(v) = v
6354                && !v.is_empty()
6355            {
6356                parts.push(v.clone());
6357            }
6358        } else if v.as_deref().unwrap_or("").is_empty() {
6359            parts.push(k.clone());
6360        }
6361    }
6362    (!parts.is_empty()).then(|| parts.join(" "))
6363}
6364
6365fn heading_style(level: u32) -> Style {
6366    // Just the role — a frontend decides how a heading of this level *looks*
6367    // (the terminal cycles a color and bolds it, the GUI scales the font). The
6368    // author wrote no emphasis here, so core records none. `level as u8` is safe:
6369    // Markdown/Djot cap headings at 6.
6370    Style::default().role(Role::Heading(level.min(255) as u8))
6371}
6372
6373/// Is this `container` node a *directive* (`:::note{…}`, `::embed{…}`,
6374/// `:vis[…]`) rather than an HTML element (`<video>`, `<picture>`, `<div>`)?
6375///
6376/// twig 2.8 folded `div`/`span`/`directive`/`element` into one `container` kind,
6377/// and left nothing that separated them: `kind`, `name` and `directive_form` all
6378/// agree, field for field, on an HTML `<div>` and a Markdown `:::div`. Leaf
6379/// answered it by sniffing the span for whichever of `:` or `<` came first.
6380/// twig 3.0 records the answer at parse time as [`ContainerOrigin`], so this is
6381/// now the parser's own knowledge rather than a guess rebuilt from the bytes it
6382/// consumed.
6383pub(crate) fn container_is_directive(node: &FlatNode) -> bool {
6384    node.origin == Some(ContainerOrigin::Directive)
6385}
6386
6387/// The tag a `container` node carries when it is an HTML element rather than a
6388/// directive — `Some("video")` for a promoted `<video>`, `None` for a `:::note`
6389/// or for any node that is not a container at all.
6390pub(crate) fn element_tag(node: &FlatNode) -> Option<&str> {
6391    (node.origin == Some(ContainerOrigin::Element))
6392        .then_some(node.name.as_deref())
6393        .flatten()
6394}
6395
6396/// A leaf directive's name and whatever attributes are not part of spelling
6397/// it — the two things a [`DirectiveMark`] carries, which twig hands back
6398/// differently per format and which a frontend must not be able to tell apart.
6399///
6400/// Markdown's `::page-break` is a `Leaf`-form directive *named* `page-break`
6401/// with no attributes, and this returns it verbatim. Djot has no leaf form:
6402/// `insert_directive` writes the same document as an empty `::: page-break`
6403/// fence, whose container is anonymous (a djot div carries no name) and whose
6404/// name arrives as the fence's one class. So where the node has no name of its
6405/// own the first `class` token *is* the name, and whatever else the class said
6406/// — an author's `::: page-break {.wide}` — stays an attribute.
6407fn leaf_directive_identity(node: &FlatNode) -> (String, Vec<(String, Option<String>)>) {
6408    let named = node.name.clone().unwrap_or_default();
6409    if !named.is_empty() {
6410        return (named, node.attrs.clone());
6411    }
6412    let class = node
6413        .attrs
6414        .iter()
6415        .find(|(k, _)| k == "class")
6416        .and_then(|(_, v)| v.as_deref())
6417        .unwrap_or_default();
6418    let mut tokens = class.split_whitespace();
6419    let Some(name) = tokens.next().map(str::to_string) else {
6420        return (named, node.attrs.clone());
6421    };
6422    let rest = tokens.collect::<Vec<_>>().join(" ");
6423    let attrs = node
6424        .attrs
6425        .iter()
6426        .filter_map(|(k, v)| {
6427            if k != "class" {
6428                return Some((k.clone(), v.clone()));
6429            }
6430            (!rest.is_empty()).then(|| (k.clone(), Some(rest.clone())))
6431        })
6432        .collect();
6433    (name, attrs)
6434}
6435
6436/// Is this inline `container` an **attributed span** — the node leaf's run-level
6437/// vocabulary rides — rather than a named directive?
6438///
6439/// The four formats spell one span four ways and twig hands the name back for
6440/// two of them: HTML's and Markdown's `<span …>` arrive named `span` with
6441/// `Element` origin, while djot's `[text]{…}` and AsciiDoc's `[.a]#text#`
6442/// arrive anonymous (an empty name) with `Directive` origin. All four are the
6443/// same node to `wrap_range_attrs`, which is what writes them, so they are the
6444/// same node here.
6445///
6446/// A *named* directive is not one, whatever its name: a Markdown `:span[…]{…}`
6447/// is a directive the parser read as a directive, twig's own
6448/// `wrap_range_attrs` says so, and it keeps the handling it has.
6449///
6450/// **Anonymous is not enough**, and the form is what finishes the question:
6451/// a djot fenced div (`{.center}` / `:::` / … / `:::`) is anonymous too, with
6452/// the same `Directive` origin, and is a *block* — `Container` form against the
6453/// span's `Text`. Reading one as a span made every gesture and every query lie
6454/// about it: `set_text_color` over a word inside such a div copied the whole
6455/// div's attribute set — its `id` along with the rest — onto the new span, and
6456/// `alignment_at_caret` reported the div's `.center` as a *run's* answer while
6457/// the walker drew none. So the anonymous arm asks the form [`is_inline`] asks.
6458pub(crate) fn is_run_span(node: &FlatNode) -> bool {
6459    if node.kind != Kind::Container {
6460        return false;
6461    }
6462    match node.name.as_deref() {
6463        None | Some("") => node.directive_form == Some(DirectiveForm::Text),
6464        Some("span") => node.origin == Some(ContainerOrigin::Element),
6465        Some(_) => false,
6466    }
6467}
6468
6469/// `base` with an attributed span's three run-level keys written over it — the
6470/// nearest-wins fold [`is_run_span`] describes, for one span. `faces` is the
6471/// build's intern table, as it is for [`Presentation::under`].
6472fn run_style(node: &FlatNode, base: Style, faces: &RefCell<FaceTable>) -> Style {
6473    Style {
6474        size: FontSize::from_attrs(&node.attrs).or(base.size),
6475        font: faces
6476            .borrow_mut()
6477            .face_from_attrs(&node.attrs)
6478            .or(base.font),
6479        color: TextColor::from_attrs(&node.attrs).or(base.color),
6480        ..base
6481    }
6482}
6483
6484pub(crate) fn is_inline(node: &FlatNode) -> bool {
6485    // A directive is inline only in its `text` form (`:name[label]{…}`); the
6486    // `leaf` and `container` forms are blocks. All three report the same `kind`,
6487    // so the form is the only thing telling them apart — and getting it wrong
6488    // costs a whole paragraph: a text directive misread as a block makes its
6489    // paragraph fail the "all children inline" test in `block`, and the line is
6490    // then walked as a container of blocks, rendering as empty rows with no
6491    // caret home at all.
6492    //
6493    // An HTML element shares the `container` kind, and twig sets the same form
6494    // on the two tags the lightweight formats have a generic spelling for: a
6495    // `<span>` is `Text` and a `<div>` is `Container`, while a `<video>` or a
6496    // `<picture>` has no form at all. So the form answers for an element as it
6497    // answers for a directive, and the origin is not consulted — which is what
6498    // makes a `<span …>` inside a paragraph an inline node.
6499    //
6500    // It has to. `wrap_range_attrs` spells an attributed run as exactly that
6501    // span in Markdown and HTML, and a paragraph holding one whose kids were
6502    // not all inline failed the test below and was walked as a container of
6503    // blocks: the text either side of the span rendered as nothing at all.
6504    if node.kind == Kind::Container {
6505        return node.directive_form == Some(DirectiveForm::Text);
6506    }
6507    is_inline_kind(&node.kind)
6508}
6509
6510/// [`is_inline`] by kind alone — for the ancestor walks, whose `QueryMatch`es
6511/// carry no `directive_form`. It answers `false` for every directive, which its
6512/// callers must (and do) reconcile: they pair it with `is_block_container`,
6513/// which claims every directive, so the pair's verdict is the same one a form
6514/// would have given. Anything looking at a *directive itself* wants [`is_inline`]
6515/// and a real node.
6516pub(crate) fn is_inline_kind(kind: &Kind) -> bool {
6517    matches!(
6518        kind,
6519        Kind::Str
6520            | Kind::SoftBreak
6521            | Kind::HardBreak
6522            | Kind::NonBreakingSpace
6523            | Kind::Emph
6524            | Kind::Strong
6525            | Kind::Mark
6526            | Kind::Insert
6527            | Kind::Delete
6528            | Kind::Verbatim
6529            | Kind::InlineMath
6530            | Kind::DisplayMath
6531            | Kind::Url
6532            | Kind::Email
6533            | Kind::Link
6534            | Kind::Image
6535            | Kind::SmartPunctuation
6536            | Kind::Superscript
6537            | Kind::Subscript
6538            | Kind::FootnoteReference
6539    )
6540}
6541
6542/// Assert two maps are identical down to every glyph, stop, and table span — the
6543/// contract `build_cached` and `build_spliced` must hold against `build`. Lives
6544/// at module scope (not in `mod tests`) so the Doc-driven differential test in
6545/// `doc.rs` can reach it and the private `stops` field it compares.
6546#[cfg(test)]
6547pub(crate) fn assert_maps_eq(a: &VisualMap, b: &VisualMap, ctx: &str) {
6548    assert_eq!(a.rows.len(), b.rows.len(), "row count ({ctx})");
6549    for (i, (ra, rb)) in a.rows.iter().zip(&b.rows).enumerate() {
6550        assert_eq!(ra.end_src, rb.end_src, "row {i} end_src ({ctx})");
6551        assert_eq!(ra.decoration, rb.decoration, "row {i} decoration ({ctx})");
6552        // The incremental walk labels a boundary from a query match's kind
6553        // string and the whole-arena walk from a `FlatNode`'s; this is what says
6554        // the two doors reach the same answer.
6555        assert_eq!(ra.boundary, rb.boundary, "row {i} boundary ({ctx})");
6556        assert_eq!(ra.code, rb.code, "row {i} code ({ctx})");
6557        assert_eq!(ra.code_lang, rb.code_lang, "row {i} code_lang ({ctx})");
6558        assert_eq!(ra.align, rb.align, "row {i} align ({ctx})");
6559        assert_eq!(
6560            ra.line_height, rb.line_height,
6561            "row {i} line_height ({ctx})"
6562        );
6563        assert_eq!(
6564            ra.glyphs.len(),
6565            rb.glyphs.len(),
6566            "row {i} glyph count ({ctx})"
6567        );
6568        for (j, (ga, gb)) in ra.glyphs.iter().zip(&rb.glyphs).enumerate() {
6569            assert_eq!(
6570                (ga.ch, ga.src, ga.stop, ga.style),
6571                (gb.ch, gb.src, gb.stop, gb.style),
6572                "row {i} glyph {j} ({ctx})"
6573            );
6574        }
6575    }
6576    assert_eq!(a.content_start, b.content_start, "content_start ({ctx})");
6577    assert_eq!(a.stops, b.stops, "stops ({ctx})");
6578    assert_eq!(a.mark_ends, b.mark_ends, "mark_ends ({ctx})");
6579    assert_eq!(a.tables.len(), b.tables.len(), "table count ({ctx})");
6580    for (i, (ta, tb)) in a.tables.iter().zip(&b.tables).enumerate() {
6581        assert_eq!(ta.rows_span, tb.rows_span, "table {i} rows_span ({ctx})");
6582        assert_eq!(ta.end_src, tb.end_src, "table {i} end_src ({ctx})");
6583    }
6584    assert_eq!(a.code_blocks, b.code_blocks, "code_blocks ({ctx})");
6585    assert_eq!(a.media, b.media, "images ({ctx})");
6586}
6587
6588#[cfg(test)]
6589mod tests {
6590    use super::*;
6591    use crate::style::{FontFamily, LineSpacing, SizeStep};
6592    use twig::{Editor, Format, NodeId};
6593
6594    fn map(src: &str) -> VisualMap {
6595        let mut ed = Editor::new_str(src, Format::Markdown).unwrap();
6596        build_t(&ed.nodes().unwrap(), src, Some(80))
6597    }
6598
6599    /// [`map`] over a Djot source. Djot is the format that spells superscript
6600    /// and subscript at all — Markdown has no syntax for either.
6601    fn map_djot(src: &str) -> VisualMap {
6602        let mut ed = Editor::new_str(src, Format::Djot).unwrap();
6603        build_t(&ed.nodes().unwrap(), src, Some(80))
6604    }
6605
6606    /// The baseline every glyph spelling `ch` was built with, in row order —
6607    /// how a test reads a raised or lowered run off the map without caring
6608    /// which row it landed on.
6609    fn baselines_of(m: &VisualMap, ch: char) -> Vec<Baseline> {
6610        m.rows
6611            .iter()
6612            .flat_map(|r| r.glyphs.iter())
6613            .filter(|g| g.ch == ch)
6614            .map(|g| g.style.baseline)
6615            .collect()
6616    }
6617
6618    /// [`map`] at a chosen wrap width.
6619    fn map_at(src: &str, wrap: Option<usize>) -> VisualMap {
6620        let mut ed = Editor::new_str(src, Format::Markdown).unwrap();
6621        build_t(&ed.nodes().unwrap(), src, wrap)
6622    }
6623
6624    /// [`map`], but with twig's `directives` extension on (off by twig's own
6625    /// default) — the `:::name{.class}` fenced-div containers leaf-core's
6626    /// `"directive"` wysiwyg arm renders.
6627    fn map_directives(src: &str) -> VisualMap {
6628        let mut ed = Editor::new_ext(
6629            src.as_bytes(),
6630            Format::Markdown,
6631            twig::MarkdownExtensions {
6632                directives: true,
6633                ..Default::default()
6634            },
6635        )
6636        .unwrap();
6637        build_t(&ed.nodes().unwrap(), src, Some(80))
6638    }
6639
6640    /// [`map`] in `format`, parsed the way every leaf document is — the
6641    /// extensions [`crate::doc::parse_extensions`] turns on, which is what
6642    /// pairs a Markdown `<div …>` with its `</div>` into a container and makes
6643    /// `::page-break` a directive rather than a paragraph of colons.
6644    fn map_leaf(src: &str, format: Format) -> VisualMap {
6645        let mut ed =
6646            Editor::new_ext(src.as_bytes(), format, crate::doc::parse_extensions()).unwrap();
6647        build_t(&ed.nodes().unwrap(), src, Some(80))
6648    }
6649
6650    /// The alignment and line spacing of every row that draws text, in order —
6651    /// how a test reads a block property off the map.
6652    fn line_facts(m: &VisualMap) -> Vec<(Option<Align>, Option<LineHeight>)> {
6653        m.rows
6654            .iter()
6655            .filter(|r| r.glyphs.iter().any(|g| !g.ch.is_whitespace()))
6656            .map(|r| (r.align, r.line_height))
6657            .collect()
6658    }
6659
6660    /// The style of the glyph spelling `ch`, first occurrence — how a test reads
6661    /// a run property off the map.
6662    fn style_of(m: &VisualMap, ch: char) -> Style {
6663        m.rows
6664            .iter()
6665            .flat_map(|r| r.glyphs.iter())
6666            .find(|g| g.ch == ch)
6667            .unwrap_or_else(|| panic!("no glyph {ch:?} in the map"))
6668            .style
6669    }
6670
6671    /// [`map`] with soft breaks preserved (`LineFlow::Preserve`).
6672    fn map_preserve(src: &str, wrap: Option<usize>) -> VisualMap {
6673        let mut ed = Editor::new_str(src, Format::Markdown).unwrap();
6674        build(
6675            &ed.nodes().unwrap(),
6676            src,
6677            wrap,
6678            true,
6679            &Surface::default(),
6680            None,
6681        )
6682    }
6683
6684    /// The cache-free reference [`build`], with no per-image height overrides —
6685    /// every block image stays its default one-row placeholder. The tests that
6686    /// need a taller image drive it through [`crate::Doc::set_media_rows`] instead.
6687    fn build_t(nodes: &[FlatNode], src: &str, wrap: Option<usize>) -> VisualMap {
6688        build(nodes, src, wrap, false, &Surface::default(), None)
6689    }
6690
6691    /// An arena and a string that disagree — spans reaching past the source they
6692    /// are built against.
6693    ///
6694    /// [`crate::Doc`] keeps the two in step, so this is a "cannot happen" that
6695    /// nonetheless *did*: `examples/bench` timed a loop of `edit_range` and then
6696    /// went on handing the grown editor's spans to a builder holding the string
6697    /// from before it, and every run ended in a slice panic rather than a
6698    /// number. `push_escaped_text` was already written to survive the mismatch —
6699    /// it clamps the span's end and falls back to an empty slice — and this is
6700    /// the half of that intent it did not carry through.
6701    ///
6702    /// Rendering the wrong thing is the acceptable answer here; panicking in a
6703    /// paint loop is not.
6704    #[test]
6705    fn a_source_shorter_than_the_arena_built_over_it_renders_rather_than_panicking() {
6706        // An escape puts the run on `push_escaped_text`'s slow path — the fast
6707        // path is a length comparison that a truncated source fails anyway.
6708        let src = "alpha \\*beta\\* gamma delta epsilon\n";
6709        let mut ed = Editor::new_str(src, Format::Markdown).unwrap();
6710        let nodes = ed.nodes().unwrap();
6711
6712        // Every truncation of it, so the cut lands before, inside and after the
6713        // escaped run rather than only where one hand-picked index put it.
6714        for cut in 0..=src.len() {
6715            if !src.is_char_boundary(cut) {
6716                continue;
6717            }
6718            let map = build_t(&nodes, &src[..cut], Some(80));
6719            for row in &map.rows {
6720                for g in &row.glyphs {
6721                    assert!(
6722                        g.src <= src.len(),
6723                        "cut {cut}: glyph {:?} points past the source at {}",
6724                        g.ch,
6725                        g.src
6726                    );
6727                }
6728            }
6729        }
6730    }
6731
6732    fn rendered(m: &VisualMap) -> String {
6733        m.rows
6734            .iter()
6735            .map(|r| r.glyphs.iter().map(Glyph::drawn).collect::<String>())
6736            .collect::<Vec<_>>()
6737            .join("\n")
6738    }
6739
6740    /// Render a source both ways: `build` over the whole marshalled arena (the
6741    /// reference), and `build_cached` driven the way [`crate::Doc`] drives it —
6742    /// top-level blocks from `child_spans`, per-block subtrees on a miss.
6743    fn render_both(
6744        ed: &mut Editor,
6745        src: &str,
6746        wrap: Option<usize>,
6747        cache: &mut BlockCache,
6748    ) -> (VisualMap, VisualMap) {
6749        let all = ed.nodes().unwrap();
6750        let surface = Surface::default();
6751        let plain = build(&all, src, wrap, false, &surface, None);
6752        let top = top_blocks(ed);
6753        let cached = build_cached(&top, src, wrap, false, &surface, None, cache, |id| {
6754            ed.subtree(NodeId(id)).unwrap_or_default()
6755        });
6756        (plain, cached)
6757    }
6758
6759    /// The whole correctness claim of the block cache: `build_cached` produces a
6760    /// byte-identical map to `build`, on a fresh cache *and* — the case that
6761    /// actually exercises reuse-and-shift plus per-block subtree marshalling — on
6762    /// a warm cache after the source has been edited underneath it.
6763    /// **Every glyph must stand on the character it claims.** A row's source
6764    /// extent is computed from its last glyph's offset, so a glyph carrying an
6765    /// offset that is not its own character's start yields a row end inside a
6766    /// multi-byte character — and every later slice of the source panics on it.
6767    ///
6768    /// Reproduces a real crash from a journal entry: a bracketed elision inside
6769    /// a blockquote (`[…]`) gave the closing bracket a `text` of `"]"` over a
6770    /// source span covering `"…]"`, because the parse folded the ellipsis into a
6771    /// neighbouring node. `push_escaped_text` walked that span assuming a
6772    /// dropped backslash was the only way text and source could diverge, so the
6773    /// `]` landed on the `…`'s first byte:
6774    /// `byte index 1236 is not a char boundary; it is inside '…'`.
6775    #[test]
6776    fn a_glyph_never_lands_inside_the_character_before_it() {
6777        let src = "> engage with it rather than look away. […]\n>\n> The through-line\n";
6778        let vmap = map(src);
6779        for (r, row) in vmap.rows.iter().enumerate() {
6780            assert!(
6781                src.is_char_boundary(row.end_src.min(src.len())),
6782                "row {r} ends at {} — inside a character",
6783                row.end_src
6784            );
6785            for g in &row.glyphs {
6786                assert!(
6787                    src.is_char_boundary(g.src.min(src.len())),
6788                    "row {r} has {:?} at {}, which is inside a character",
6789                    g.ch,
6790                    g.src
6791                );
6792            }
6793        }
6794        // The elision survives, and its bracket sits on the real `]`.
6795        let text: String = vmap
6796            .rows
6797            .iter()
6798            .flat_map(|r| r.glyphs.iter().map(|g| g.ch))
6799            .collect();
6800        assert!(text.contains("[…]"), "the elision should render: {text:?}");
6801        let close = vmap
6802            .rows
6803            .iter()
6804            .flat_map(|r| r.glyphs.iter())
6805            .find(|g| g.ch == ']')
6806            .expect("a closing bracket");
6807        assert_eq!(
6808            src[close.src..].chars().next(),
6809            Some(']'),
6810            "the bracket glyph should stand on the source's own `]`"
6811        );
6812    }
6813
6814    #[test]
6815    fn build_cached_matches_build() {
6816        let docs = [
6817            "# Title\n\nThe quick brown fox.\n\nAnother paragraph here.\n",
6818            "## H\n\n- one\n- two\n- three\n\n> a quote\n> continued\n",
6819            "para one\n\n```\ncode\nlines\n```\n\nafter code\n",
6820            "| a | b |\n|---|---|\n| 1 | 2 |\n\ntext after a table\n",
6821            "line\n- \nsetext?\n\nreal para\n\n\n\ntrailing blanks\n",
6822            "> quote with **bold** and a [link](https://x.dev)\n>\n> - item\n> - item2\n\ntail\n",
6823            "intro\n\n![a cat](img/cat.png)\n\nbetween\n\n![](https://x.dev/logo.svg)\n\nend\n",
6824            "- text item\n- ![alt](pic.png)\n- more text\n",
6825            // Footnotes: twig parses each definition as a root beside `doc`, so
6826            // these are the docs where the reference build and the incremental
6827            // one could disagree about what the top-level blocks even are.
6828            "A claim[^1] and another[^src].\n\n[^1]: First note.\n\n[^src]: Second.\n\ntail\n",
6829            "note[^a]\n\n[^a]: body **bold**\n    wrapped on\n    three lines\n\nafter\n",
6830            // No trailing newline. twig closes the document's last block on the
6831            // virtual newline it supplies at EOF, so that block's `span.end` is
6832            // `source.len() + 1` — a range that slices no bytes at all. Keying
6833            // the block cache off such a slice made every last block hash alike;
6834            // see [`block_bytes`].
6835            "# Title\n\nThe quick brown fox.\n\nA tail with no newline",
6836            "A claim[^1] and another[^src].\n\n[^1]: First note.\n[^src]: Second, ending the file.",
6837            // Comments draw nothing. The per-block builder the cached path
6838            // renders one with starts at offset 0 and, drawing nothing, never
6839            // moved — so the walk went on from 0 and spelled every line of the
6840            // document as a blank row. One at the start, one between blocks,
6841            // one at the end, so each position is covered.
6842            "<!-- lead -->\n\npara\n\n<!-- exec -->\n```\ncode\n```\n\nafter\n\n<!-- trail -->\n",
6843            // A Markdown `<div>` ends with a hidden `</div>` line the walk
6844            // steps over — between blocks and closing the file, so both the
6845            // separator after it and the trailing count are covered.
6846            "above\n\n<div class=\"center\">\n\nhello\n\n</div>\n\nbelow\n",
6847            "above\n\n<div class=\"center\">\n\nhello\n\nworld\n\n</div>\n",
6848            // Link reference definitions: roots beside `doc` like footnotes,
6849            // but drawing nothing. Alone between blocks, glued under a
6850            // paragraph, and closing the file under a comment — the README
6851            // shape.
6852            "see [a] and [b]\n\n[a]: /a\n\nmid\n[b]: /b \"bee\"\n\nend [c]\n\n<!-- links -->\n[c]: /c\n",
6853            // A rule's row ends past the newline under it while the walk
6854            // stands at the rule, and the rule's block is never cached for
6855            // it: an empty line under one mid-document and closing it.
6856            "para\n\n---\n\n\n\nbetween rules\n\n---\n\n",
6857            // Blank lines above the first block draw rows: bare, past
6858            // frontmatter, and past a comment that draws nothing.
6859            "\n\n\nfirst\n\nsecond\n",
6860            "---\ntitle: x\n---\n\n\nafter frontmatter\n",
6861            "<!-- lead -->\n\n\n\nafter a comment\n",
6862            // An empty paragraph at the end of a div draws inside it.
6863            "<div data-line-height=\"1.5\">\n\nI cry\n\nknees\n\n\n\n</div>\n\nafter\n",
6864        ];
6865        for wrap in [None, Some(80usize), Some(20)] {
6866            for src in docs {
6867                let ctx = format!("wrap={wrap:?} src={src:?}");
6868                let mut ed = Editor::new_str(src, Format::Markdown).unwrap();
6869                let mut cache = BlockCache::default();
6870
6871                // 1) Fresh cache equals the cache-free build.
6872                let (plain, cached) = render_both(&mut ed, src, wrap, &mut cache);
6873                assert_maps_eq(&plain, &cached, &format!("fresh {ctx}"));
6874
6875                // 2) Type a char mid-document, reparse, rebuild with the now-warm
6876                //    cache: the edited block is re-marshalled and re-rendered,
6877                //    every block below it is reused shifted, and the result must
6878                //    still match a from-scratch build.
6879                let at = (src.len() / 2..=src.len())
6880                    .find(|&i| src.is_char_boundary(i))
6881                    .unwrap();
6882                ed.edit_range(at, at, "Z").unwrap();
6883                let src2 = ed.source_str().unwrap();
6884                let (plain2, cached2) = render_both(&mut ed, &src2, wrap, &mut cache);
6885                assert_maps_eq(&plain2, &cached2, &format!("after insert {ctx}"));
6886
6887                // 3) Delete it again: offsets shift back the other way, and the
6888                //    warm cache must not hand back stale shifted rows.
6889                ed.edit_range(at, at + 1, "").unwrap();
6890                let src3 = ed.source_str().unwrap();
6891                let (plain3, cached3) = render_both(&mut ed, &src3, wrap, &mut cache);
6892                assert_maps_eq(&plain3, &cached3, &format!("after delete {ctx}"));
6893            }
6894        }
6895    }
6896
6897    /// A document that does not end in a newline is the one place twig hands
6898    /// leaf a top-level span that addresses no source: the last block is closed
6899    /// on the virtual newline the parser supplies at EOF, so its `span.end` is
6900    /// `source.len() + 1`. The block cache keys on the bytes under that span, and
6901    /// reading the out-of-range slice as *no bytes* broke it two ways at once —
6902    /// [`block_bytes`] has the full account. Both ways are checked here, because
6903    /// they fail independently.
6904    #[test]
6905    fn a_block_running_past_the_last_byte_still_keys_the_cache_by_its_own_bytes() {
6906        // One: two overrunning blocks collide. A footnote definition is a root
6907        // beside `doc` that [`top_blocks`] merges into the top level, while the
6908        // `section` above it spans the definition's bytes too — so when the
6909        // definition ends the file, both blocks end past it. The second was
6910        // served the first's rows, and the definition rendered as a copy of the
6911        // heading.
6912        let src = "A claim[^1] worth checking.\n\n# A heading with a reference[^1] in it\n\n[^1]: The first note.\n[^note]: A note with a word for a label.";
6913        let mut ed = Editor::new_str(src, Format::Djot).unwrap();
6914        let (plain, cached) = render_both(&mut ed, src, Some(80), &mut BlockCache::default());
6915        assert_maps_eq(&plain, &cached, "a definition ending the file");
6916        let text = rendered(&cached);
6917        assert!(
6918            text.ends_with("[note] A note with a word for a label."),
6919            "the last definition should render itself: {text:?}"
6920        );
6921        assert_eq!(
6922            text.matches("A heading with a reference").count(),
6923            1,
6924            "the heading should render exactly once: {text:?}"
6925        );
6926
6927        // Two: one overrunning block goes stale. Its bytes are its cache key, so
6928        // a block that keeps hashing the same however it is edited is served the
6929        // rows built before the edit — the whole last line frozen as the user
6930        // types in it.
6931        let mut cache = BlockCache::default();
6932        let first = "first para\n\n# A heading\n\nlast para with no newline";
6933        let mut ed = Editor::new_str(first, Format::Djot).unwrap();
6934        let (_, warm) = render_both(&mut ed, first, Some(80), &mut cache);
6935        assert!(rendered(&warm).ends_with("last para with no newline"));
6936
6937        let second = "first para\n\n# A heading\n\nDIFFERENT text without a newline";
6938        let mut ed = Editor::new_str(second, Format::Djot).unwrap();
6939        let (plain, cached) = render_both(&mut ed, second, Some(80), &mut cache);
6940        assert_maps_eq(&plain, &cached, "edited last block, warm cache");
6941        let text = rendered(&cached);
6942        assert!(
6943            text.ends_with("DIFFERENT text without a newline"),
6944            "the warm cache served the pre-edit rows: {text:?}"
6945        );
6946    }
6947
6948    #[test]
6949    fn resolves_markup_to_plain_text() {
6950        let text = rendered(&map("# Title\n\na **bold** word\n"));
6951        assert!(!text.contains('#'), "heading marker shown: {text:?}");
6952        assert!(!text.contains("**"), "strong delimiters shown: {text:?}");
6953        assert!(text.contains("Title") && text.contains("bold word"));
6954    }
6955
6956    #[test]
6957    fn every_glyph_points_at_its_source_byte() {
6958        let src = "a **bold** c\n";
6959        let m = map(src);
6960        for row in &m.rows {
6961            for g in &row.glyphs {
6962                // A real (non-synthetic) glyph's source byte is the glyph's char.
6963                if g.src < src.len()
6964                    && src.is_char_boundary(g.src)
6965                    && let Some(sc) = src[g.src..].chars().next()
6966                    && sc == g.ch
6967                {
6968                    continue;
6969                }
6970                // Synthetic prefixes (none here) would be the only exceptions.
6971                panic!("glyph {:?} at src {} doesn't match source", g.ch, g.src);
6972            }
6973        }
6974    }
6975
6976    #[test]
6977    fn offset_and_position_round_trip_on_visible_text() {
6978        let m = map("hello world\n");
6979        let (r, c) = m.pos_of_offset(6); // the 'w'
6980        assert_eq!(m.offset_of_pos(r, c), 6);
6981    }
6982
6983    #[test]
6984    fn visible_utf16_indices_count_the_text_the_system_sees() {
6985        // Hidden delimiters, a two-unit emoji, and a block gap — every way the
6986        // visible text's UTF-16 length parts company with a source byte count.
6987        let src = "a **b\u{1F600}** c\n\nd\n";
6988        let m = map(src);
6989        let end = m.snap_to_stop(src.len());
6990        let text = m.visible_text(0, end);
6991        assert_eq!(text, "a b\u{1F600} c\nd");
6992
6993        // Forward: the index of each offset is where that character sits in
6994        // the visible string, in UTF-16 units.
6995        for (i, (src_off, _)) in m.visible_items(0, end).iter().enumerate() {
6996            let expect: usize = text.chars().take(i).map(char::len_utf16).sum();
6997            assert_eq!(
6998                m.visible_utf16_len(0, *src_off),
6999                expect,
7000                "utf16 index of source offset {src_off}"
7001            );
7002            // And back: the index resolves to the offset it came from.
7003            assert_eq!(m.offset_at_visible_utf16(end, expect), Some(*src_off));
7004        }
7005        // Inside the emoji's surrogate pair resolves to the emoji.
7006        let emoji_src = src.find('\u{1F600}').unwrap();
7007        let emoji_idx = m.visible_utf16_len(0, emoji_src);
7008        assert_eq!(
7009            m.offset_at_visible_utf16(end, emoji_idx + 1),
7010            Some(emoji_src)
7011        );
7012        // At or past the end is nobody's character.
7013        let total = m.visible_utf16_len(0, end);
7014        assert_eq!(total, text.encode_utf16().count());
7015        assert_eq!(m.offset_at_visible_utf16(end, total), None);
7016    }
7017
7018    /// The documents the lookups are checked against their reference scans
7019    /// on: every shape that puts rows out of source order or a stop out of
7020    /// step with a glyph. A wrapped table, whose cells' second lines sit
7021    /// below the next column's first; a wide grapheme and an emoji outside
7022    /// the BMP; hidden delimiters and a link's hidden destination; a list
7023    /// with synthetic markers; a code block; an image row whose label glyphs
7024    /// all share one offset; an empty paragraph, a heading, and a wrapped
7025    /// paragraph — at a narrow width so the table and the prose both wrap,
7026    /// and unwrapped, which is how a GUI builds it.
7027    fn lookup_maps() -> Vec<(String, VisualMap)> {
7028        let table = "| left cell that wraps | right |\n|---|---|\n| a longer cell than the column can hold | b |\n| `k` | 你好 |\n\n";
7029        let srcs = [
7030            "hello world\n",
7031            "a **b\u{1F600}** c\n\nd\n",
7032            "- one *two*\n- three\n\n\n# Title\n\nend [link](https://e.org/x) tail\n",
7033            &format!("{table}```\nx\ny\n```\n\n![alt](a.png)\n\ntail 你好 **bold** here\n"),
7034            "one two three four five six seven eight nine ten eleven twelve thirteen\n\n| a | b |\n|-|-|\n| c d e f g h | i |\n",
7035        ];
7036        let mut out = Vec::new();
7037        for src in srcs {
7038            for wrap in [Some(14), None] {
7039                out.push((format!("{src:?} at {wrap:?}"), map_at(src, wrap)));
7040            }
7041        }
7042        out
7043    }
7044
7045    /// `pos_of_offset` as it was written before the walk learnt to dismiss a
7046    /// row from its ends: every row read through, the nearest stop kept.
7047    fn pos_of_offset_by_scan(m: &VisualMap, off: usize) -> (usize, usize) {
7048        let mut best: Option<(usize, usize, usize)> = None;
7049        for (r, row) in m.rows.iter().enumerate() {
7050            if row.decoration {
7051                continue;
7052            }
7053            let cand = row
7054                .glyphs
7055                .iter()
7056                .enumerate()
7057                .find(|(_, g)| g.stop && g.src >= off)
7058                .map(|(i, g)| (g.src, r, row.col_of_glyph(i)))
7059                .or_else(|| (row.end_src >= off).then_some((row.end_src, r, row.width())));
7060            if let Some(c) = cand
7061                && best.is_none_or(|b| c.0 <= b.0)
7062            {
7063                best = Some(c);
7064            }
7065        }
7066        match best {
7067            Some((_, r, c)) => (r, c),
7068            None => {
7069                let r = m.last_stop_row();
7070                (r, m.row_width(r))
7071            }
7072        }
7073    }
7074
7075    /// `visible_items` as it was written before the spelling was tabulated:
7076    /// every stop glyph in the range gathered, sorted, and paired up.
7077    fn visible_items_by_scan(m: &VisualMap, from: usize, to: usize) -> Vec<(usize, Option<char>)> {
7078        let (lo, hi) = m.visible_span(from, to);
7079        let from = m.snap_to_glyph_stop(from);
7080        let mut glyphs: Vec<(usize, char)> = m
7081            .rows
7082            .iter()
7083            .filter(|r| !r.decoration)
7084            .flat_map(|r| r.glyphs.iter())
7085            .filter(|g| g.stop && g.src >= from && g.src < to)
7086            .map(|g| (g.src, g.ch))
7087            .collect();
7088        glyphs.sort_by_key(|&(src, _)| src);
7089        glyphs.dedup_by_key(|&mut (src, _)| src);
7090        let cell_ends: Vec<usize> = m
7091            .tables
7092            .iter()
7093            .flat_map(|t| t.grid.iter())
7094            .flat_map(|r| r.cells.iter())
7095            .map(|c| c.end)
7096            .collect();
7097        m.stops[lo..hi]
7098            .iter()
7099            .map(|&s| {
7100                let ch = glyphs
7101                    .iter()
7102                    .find(|&&(src, _)| src == s)
7103                    .filter(|_| !cell_ends.contains(&s))
7104                    .map(|&(_, ch)| ch);
7105                (s, ch)
7106            })
7107            .collect()
7108    }
7109
7110    #[test]
7111    fn pos_of_offset_agrees_with_reading_every_row_through() {
7112        for (name, m) in lookup_maps() {
7113            let len = m.rows.iter().map(|r| r.end_src).max().unwrap_or(0) + 2;
7114            for off in 0..=len {
7115                assert_eq!(
7116                    m.pos_of_offset(off),
7117                    pos_of_offset_by_scan(&m, off),
7118                    "offset {off} of {name}"
7119                );
7120            }
7121        }
7122    }
7123
7124    #[test]
7125    fn the_tabulated_spelling_agrees_with_gathering_the_glyphs() {
7126        for (name, m) in lookup_maps() {
7127            let end = m.stops.last().copied().unwrap_or(0);
7128            // Whole text, and every window a frontend might ask for.
7129            let mut spans = vec![(0, end)];
7130            for &a in m.stops.iter().step_by(3) {
7131                spans.push((a, end));
7132                spans.push((0, a));
7133                spans.push((a, (a + 5).min(end)));
7134            }
7135            for (from, to) in spans {
7136                let items = visible_items_by_scan(&m, from, to);
7137                assert_eq!(
7138                    m.visible_items(from, to),
7139                    items,
7140                    "items {from}..{to} of {name}"
7141                );
7142                let utf16: usize = items
7143                    .iter()
7144                    .map(|(_, ch)| ch.map_or(1, char::len_utf16))
7145                    .sum();
7146                assert_eq!(
7147                    m.visible_utf16_len(from, to),
7148                    utf16,
7149                    "utf16 {from}..{to} of {name}"
7150                );
7151            }
7152            // And back: every UTF-16 index in the text resolves to the stop
7153            // that spells it, as the scan would have found it.
7154            let items = visible_items_by_scan(&m, 0, end);
7155            let mut seen = 0;
7156            for (src, ch) in &items {
7157                for u in seen..seen + ch.map_or(1, char::len_utf16) {
7158                    assert_eq!(
7159                        m.offset_at_visible_utf16(end, u),
7160                        Some(*src),
7161                        "index {u} of {name}"
7162                    );
7163                }
7164                seen += ch.map_or(1, char::len_utf16);
7165            }
7166            assert_eq!(
7167                m.offset_at_visible_utf16(end, seen),
7168                None,
7169                "past the end of {name}"
7170            );
7171        }
7172    }
7173
7174    #[test]
7175    fn visible_text_spends_exactly_one_character_on_every_stop() {
7176        // A list (whose items' ends no gap row follows), a table (whose cells'
7177        // ends draw a gutter space), and a code block (one row per line):
7178        // every place the text used to part company with the stop count, in
7179        // both directions. `UITextInput`'s tokenizer indexes this text by
7180        // that count, so the two must agree exactly between any two stops.
7181        let src = "- one\n- two\n\n| a | b |\n| - | - |\n| c | d |\n\n```\nx\ny\n```\n\nend\n";
7182        let m = map(src);
7183        let end = m.snap_to_stop(src.len());
7184        // The table's trailing stop draws no glyph, so it is spelled as a line
7185        // end too: to the system the table ends on a blank line, which is
7186        // where the caret past it stands.
7187        assert_eq!(m.visible_text(0, end), "one\ntwo\na\nb\nc\nd\n\nx\ny\nend");
7188        // Between any two stops, one character per hop.
7189        let first = m.snap_to_glyph_stop(0);
7190        let stops: Vec<usize> = std::iter::successors(Some(first), |&o| m.stop_after(o)).collect();
7191        for (i, &a) in stops.iter().enumerate() {
7192            for (j, &b) in stops.iter().enumerate().skip(i) {
7193                assert_eq!(
7194                    m.visible_text(a, b).chars().count(),
7195                    j - i,
7196                    "text between stops {a} and {b}"
7197                );
7198            }
7199        }
7200        // A cell's end is spelled as a line end, not the space it draws, so a
7201        // tap landing past `a`'s last letter has nothing to step over into `b`.
7202        let a_end = src.find("a |").unwrap() + 1;
7203        assert_eq!(m.visible_text(a_end, a_end + 1), "\n");
7204    }
7205
7206    #[test]
7207    fn unwrapped_mode_emits_one_row_per_paragraph() {
7208        // A long paragraph that would wrap under a column budget stays a single
7209        // row when wrap is None (the GUI wraps it at pixel width instead).
7210        let long = "one two three four five six seven eight nine ten eleven twelve\n";
7211        let mut ed = Editor::new_str(long, Format::Markdown).unwrap();
7212        let wrapped = build_t(&ed.nodes().unwrap(), long, Some(12));
7213        let unwrapped = build_t(&ed.nodes().unwrap(), long, None);
7214        assert!(wrapped.num_rows() > 1, "narrow column should wrap");
7215        assert_eq!(unwrapped.num_rows(), 1, "no budget should keep it one row");
7216        // Every glyph's source byte is preserved in the single row.
7217        let text: String = unwrapped.rows[0].glyphs.iter().map(|g| g.ch).collect();
7218        assert_eq!(text.trim_end(), long.trim_end());
7219    }
7220
7221    fn line_texts(m: &VisualMap) -> Vec<String> {
7222        m.rows
7223            .iter()
7224            .map(|r| {
7225                // Trim the trailing whitespace a row may carry — the zero-width
7226                // '\n' that closes a preserved line, and any space glyph left at
7227                // a wrap boundary (both real caret stops, neither visible text).
7228                r.glyphs
7229                    .iter()
7230                    .map(|g| g.ch)
7231                    .collect::<String>()
7232                    .trim_end()
7233                    .to_string()
7234            })
7235            .collect()
7236    }
7237
7238    #[test]
7239    fn preserve_lays_each_soft_break_on_its_own_row() {
7240        // A soft break (a bare newline inside a paragraph) folds into a space by
7241        // default — the whole paragraph is one reflowed row...
7242        let src = "one two\nthree four\n";
7243        let mut ed = Editor::new_str(src, Format::Markdown).unwrap();
7244        let folded = build_t(&ed.nodes().unwrap(), src, None);
7245        assert_eq!(folded.num_rows(), 1, "fold: one reflowed row");
7246        assert_eq!(
7247            line_texts(&folded),
7248            vec!["one two three four"],
7249            "break folded to a space"
7250        );
7251
7252        // ...and under Preserve it renders where it was written, a row per line.
7253        let kept = map_preserve(src, None);
7254        assert_eq!(
7255            line_texts(&kept),
7256            vec!["one two", "three four"],
7257            "preserve: a row per line"
7258        );
7259    }
7260
7261    #[test]
7262    fn a_preserved_break_keeps_the_newline_offset_as_a_caret_stop() {
7263        // The break must leave a caret stop at the newline byte, or the caret
7264        // could not rest at the end of the first line. The '\n' glyph is dropped
7265        // from the row (so nothing stray renders); its offset (7 here) becomes the
7266        // row's end stop instead — the same offset the folded space would carry.
7267        let src = "one two\nthree four\n";
7268        let m = map_preserve(src, None);
7269        assert!(
7270            !m.rows[0].glyphs.iter().any(|g| g.ch == '\n'),
7271            "the break glyph is dropped"
7272        );
7273        assert_eq!(
7274            m.rows[0].end_src, 7,
7275            "the first row ends at the newline byte"
7276        );
7277        assert!(m.is_stop(7), "the newline offset is a caret stop");
7278        // Row end offsets stay strictly ascending — no two rows pin one offset.
7279        let offs: Vec<usize> = m.rows.iter().map(|r| r.end_src).collect();
7280        assert!(
7281            offs.windows(2).all(|w| w[0] < w[1]),
7282            "offsets not unique: {offs:?}"
7283        );
7284    }
7285
7286    #[test]
7287    fn preserved_lines_wrap_independently() {
7288        // Each preserved line wraps to the column on its own; the break between
7289        // them is hard, so a word never crosses it — "gamma" and "delta" could
7290        // share a row on width alone but the soft break keeps them apart.
7291        let src = "alpha beta gamma\ndelta epsilon\n";
7292        let m = map_preserve(src, Some(12));
7293        assert_eq!(
7294            line_texts(&m),
7295            vec!["alpha beta", "gamma", "delta", "epsilon"],
7296            "each source line wraps on its own"
7297        );
7298    }
7299
7300    #[test]
7301    fn an_empty_paragraph_between_blocks_renders_its_own_rows() {
7302        // "A", then two blank lines (an empty paragraph opened with Enter), then
7303        // "B": the empty paragraph must be navigable rows, not collapsed onto B.
7304        // Rows: "A", spacer, empty-paragraph, spacer, "B" — each blank row a
7305        // distinct source offset.
7306        let m = map("A\n\n\n\nB\n");
7307        let text: Vec<String> = m
7308            .rows
7309            .iter()
7310            .map(|r| r.glyphs.iter().map(|g| g.ch).collect())
7311            .collect();
7312        assert_eq!(text, vec!["A", "", "", "", "B"], "got {text:?}");
7313        let offs: Vec<usize> = m.rows.iter().map(|r| r.end_src).collect();
7314        // Strictly ascending — no two rows share an offset (else the caret pins).
7315        assert!(
7316            offs.windows(2).all(|w| w[0] < w[1]),
7317            "offsets not unique: {offs:?}"
7318        );
7319    }
7320
7321    #[test]
7322    fn a_tight_block_boundary_still_gets_one_separator() {
7323        // A heading directly above text (no blank line between) keeps the single
7324        // conventional separator row, as before.
7325        let m = map("# H\ntext\n");
7326        let text: Vec<String> = m
7327            .rows
7328            .iter()
7329            .map(|r| r.glyphs.iter().map(|g| g.ch).collect())
7330            .collect();
7331        assert_eq!(text, vec!["H", "", "text"], "got {text:?}");
7332    }
7333
7334    #[test]
7335    fn an_escaped_delimiter_renders_without_its_backslash_and_maps_true_offsets() {
7336        // `a\*b` renders the three visible chars `a * b` — the escape backslash
7337        // is hidden — and every glyph points at its real source byte, so a caret
7338        // past the escape lands right (the `*` at source 2, `b` at source 3, not
7339        // the drifted 1/2 the naive text-offset mapping gave).
7340        let m = map("a\\*b\n");
7341        let row: Vec<(char, usize)> = m.rows[0].glyphs.iter().map(|g| (g.ch, g.src)).collect();
7342        assert_eq!(row, vec![('a', 0), ('*', 2), ('b', 3)], "got {row:?}");
7343    }
7344
7345    #[test]
7346    fn an_escaped_hash_stays_a_paragraph_and_shows_the_hash() {
7347        // `\# hi` is a paragraph beginning with a literal `#`, not a heading —
7348        // the backslash is hidden, the `#` shown at its true offset.
7349        let m = map("\\# hi\n");
7350        let text: String = m.rows[0].glyphs.iter().map(|g| g.ch).collect();
7351        assert_eq!(text, "# hi");
7352        assert_eq!(
7353            m.rows[0].glyphs[0].src, 1,
7354            "the # is at source byte 1, past the \\"
7355        );
7356    }
7357
7358    #[test]
7359    fn a_tight_nested_list_hangs_its_sublist_directly_under_the_item() {
7360        // A list item's own text and the sub-list nested under it are written on
7361        // adjacent source lines, so the rich view butts them together — no
7362        // fabricated blank row. Regression: the synthetic "breathe" separator
7363        // used to open a gap between `• a` and its `  • b`.
7364        assert_eq!(rendered(&map("- a\n  - b\n")), "• a\n  • b");
7365    }
7366
7367    #[test]
7368    fn a_loose_nested_list_keeps_its_real_blank_line() {
7369        // A genuine blank source line (a loose list) still parts the item from
7370        // its sub-list — only the *fabricated* separator is suppressed, never a
7371        // real one the author typed. The gap row wears the item's continuation
7372        // prefix (the two-space indent), so it renders as "  ", not empty.
7373        assert_eq!(rendered(&map("- a\n\n  - b\n")), "• a\n  \n  • b");
7374    }
7375
7376    #[test]
7377    fn a_code_block_in_a_list_item_wears_one_marker() {
7378        // The item's marker goes on the block's first line and its indent on
7379        // the rest, as a wrapped paragraph's rows do. Every line wore the
7380        // marker once, so a two-line block in an item read as two items.
7381        assert_eq!(
7382            rendered(&map("- ```\n  one\n  two\n  ```\n")),
7383            "• one\n  two"
7384        );
7385        assert_eq!(
7386            rendered(&map("1. ```\n   one\n   two\n   ```\n")),
7387            "1. one\n   two"
7388        );
7389        assert_eq!(
7390            rendered(&map("- [ ] ```\n  one\n  two\n  ```\n")),
7391            "☐ one\n  two"
7392        );
7393    }
7394
7395    #[test]
7396    fn an_items_later_rows_wear_its_marker_as_a_blank_indent() {
7397        // Spelled with the marker's characters so a proportional face gives
7398        // the indent exactly the marker's width; drawn blank everywhere.
7399        for (src, marker) in [
7400            ("- a\n\n  b\n", "• "),
7401            ("1. a\n\n   b\n", "1. "),
7402            ("- [ ] a\n\n  b\n", "☐ "),
7403        ] {
7404            let m = map(src);
7405            let last = m.rows.last().unwrap();
7406            let indent: String = last
7407                .glyphs
7408                .iter()
7409                .take_while(|g| g.style.role == Role::ListIndent)
7410                .map(|g| g.ch)
7411                .collect();
7412            assert_eq!(indent, marker, "{src:?}");
7413            assert!(rendered(&m).ends_with(&format!("{}b", " ".repeat(marker.chars().count()))));
7414        }
7415    }
7416
7417    #[test]
7418    fn a_code_block_in_a_quote_draws_no_row_for_its_closing_fence() {
7419        // The gutter is the same on every row. The closing fence is markup,
7420        // and drew an empty quoted line under the code as if the writer had
7421        // typed one, at the end of the document or before more.
7422        assert_eq!(
7423            rendered(&map("> ```\n> one\n> two\n> ```\n")),
7424            "│ one\n│ two"
7425        );
7426        assert_eq!(
7427            rendered(&map("> ```\n> one\n> ```\n\nafter\n")),
7428            "│ one\n\nafter"
7429        );
7430        // A quoted line the writer added under the fence is still one.
7431        assert_eq!(rendered(&map("> ```\n> one\n> ```\n>\n")), "│ one\n│ ");
7432    }
7433
7434    #[test]
7435    fn frontmatter_is_hidden_and_the_document_opens_into_its_content() {
7436        // Leading YAML frontmatter renders nothing — no phantom blank rows for
7437        // its lines, no leading gap — and `content_start` points at the first
7438        // real block so the caret floor can keep out of the hidden metadata.
7439        let fm = "---\nconfig: prov.yaml\ncontents:\n- '[Sample](sample.md)'\n---\n";
7440        let src = format!("{fm}# leaf\n\nA line.\n");
7441        let m = map(&src);
7442        let text = rendered(&m);
7443        assert!(
7444            !text.contains("config"),
7445            "frontmatter body leaked: {text:?}"
7446        );
7447        assert!(!text.contains("prov"), "frontmatter body leaked: {text:?}");
7448        assert_eq!(
7449            m.rows[0].glyphs.iter().map(|g| g.ch).collect::<String>(),
7450            "leaf"
7451        );
7452        assert_eq!(
7453            m.content_start,
7454            fm.len(),
7455            "floor should be the first real block"
7456        );
7457    }
7458
7459    #[test]
7460    fn a_frontmatter_only_document_puts_the_floor_after_the_frontmatter() {
7461        // Nothing to render, so the caret floor is the end of the hidden
7462        // frontmatter — not 0, which is *before* the opening `---` and made the
7463        // first keystroke in a fresh metadata-only note land ahead of it. And
7464        // the frontmatter's own newlines are not trailing blank lines: they used
7465        // to open phantom rows at offsets 1..4, inside the metadata.
7466        let src = "---\ntitle: 2026-08-29\nid: f8s32cd\n---\n";
7467        let m = map(src);
7468        assert_eq!(m.content_start, src.len(), "floor must clear the metadata");
7469        assert!(
7470            m.rows.is_empty(),
7471            "frontmatter must render no rows: {:?}",
7472            rendered(&m)
7473        );
7474        assert!(
7475            m.stops.is_empty(),
7476            "no stop may sit inside the metadata: {:?}",
7477            m.stops
7478        );
7479    }
7480
7481    #[test]
7482    fn a_frontmatter_only_document_still_counts_its_real_blank_lines() {
7483        // Two blank lines after the frontmatter are the author's empty paragraph
7484        // and still render, counted from the metadata's end rather than from 0.
7485        let fm = "---\ntitle: n\n---\n";
7486        let m = map(&format!("{fm}\n\n"));
7487        assert_eq!(m.content_start, fm.len());
7488        assert_eq!(m.rows.len(), 2, "the two trailing newlines each open a row");
7489        assert!(
7490            m.rows.iter().all(|r| r.end_src > fm.len()),
7491            "rows must sit past the frontmatter"
7492        );
7493    }
7494
7495    #[test]
7496    fn a_document_without_frontmatter_has_a_zero_floor() {
7497        let m = map("# leaf\n\nbody\n");
7498        assert_eq!(m.content_start, 0);
7499    }
7500
7501    #[test]
7502    fn trailing_spaces_become_caret_stops_so_the_caret_can_be_drawn_past_them() {
7503        // Markdown/Djot drop the trailing space in `hello ` from the `str` node,
7504        // so without help the row would end at `hello` and the caret couldn't be
7505        // drawn past column 5 — typing a space at a line's end wouldn't move it
7506        // on screen until the next visible character reparsed the space into an
7507        // interior node. The builder recovers it from the block's span/content_span
7508        // gap and emits it as a real, caret-stoppable glyph.
7509        let m = map("hello \n");
7510        assert_eq!(
7511            m.rows[0].glyphs.iter().map(|g| g.ch).collect::<String>(),
7512            "hello "
7513        );
7514        assert_eq!(
7515            m.rows[0].end_src, 6,
7516            "the row now ends past the trailing space"
7517        );
7518        // The caret can rest both on and past the space.
7519        assert_eq!(m.pos_of_offset(5), (0, 5), "between 'o' and the space");
7520        assert_eq!(m.pos_of_offset(6), (0, 6), "past the space");
7521        // Two trailing spaces, both stops.
7522        let m = map("hello  \n");
7523        assert_eq!(
7524            m.rows[0].glyphs.iter().map(|g| g.ch).collect::<String>(),
7525            "hello  "
7526        );
7527        assert_eq!(m.pos_of_offset(7), (0, 7));
7528    }
7529
7530    #[test]
7531    fn a_headings_trailing_space_is_a_caret_stop_too() {
7532        // The hidden `# ` marker means `# hi ` renders as `hi ` in three columns;
7533        // the caret past the trailing space lands on the third.
7534        let m = map("# hi \n");
7535        assert_eq!(
7536            m.rows[0].glyphs.iter().map(|g| g.ch).collect::<String>(),
7537            "hi "
7538        );
7539        assert_eq!(m.pos_of_offset(5), (0, 3));
7540    }
7541
7542    #[test]
7543    fn a_table_cells_trailing_padding_is_not_mistaken_for_block_trailing_space() {
7544        // A cell's own `span` is the whole row, so the trailing-whitespace
7545        // recovery must not run for cells or it would swallow the `│` delimiters
7546        // and neighbours between the cell text and the row's end. The grid stays
7547        // exactly as before.
7548        let text = rendered(&map(TABLE));
7549        assert!(
7550            text.contains("│ Pear │   3 │"),
7551            "cell padding disturbed:\n{text}"
7552        );
7553    }
7554
7555    #[test]
7556    fn a_click_below_the_last_row_lands_on_the_last_stop_not_offset_zero() {
7557        // A drag into the empty space under a short document used to resolve to
7558        // offset 0 — the wrong direction, and not even a caret stop when the
7559        // document opens on hidden frontmatter (its `content_start` floor is not
7560        // a stop), which crashed the caret invariant. It now lands on the last
7561        // stop: the end of the document, where dragging downward should reach.
7562        let fm = "---\ntitle: n\n---\n";
7563        let m = map(&format!("{fm}# Hi\n\nbody\n"));
7564        let below = m.num_rows() + 5;
7565        let off = m.offset_of_pos(below, 0);
7566        assert!(
7567            m.is_stop(off),
7568            "offset {off} from a below-content click is not a stop"
7569        );
7570        assert_eq!(
7571            off,
7572            m.stops.last().copied().unwrap(),
7573            "should be the document's last stop"
7574        );
7575        assert!(
7576            off > fm.len(),
7577            "must not fall onto the hidden frontmatter floor"
7578        );
7579    }
7580
7581    #[test]
7582    fn offset_of_pos_is_a_stop_for_every_row_including_past_the_end() {
7583        // The invariant the caret motion asserts: whatever cell a click names,
7584        // the offset it resolves to is one the caret can actually rest at.
7585        for src in [
7586            "hello \n",
7587            "# A heading here \n\nbody text goes on \n",
7588            "---\nk: v\n---\n# Title\n\nprose here that wraps a bit \n",
7589        ] {
7590            let m = map(src);
7591            for row in 0..m.num_rows() + 3 {
7592                for col in 0..30 {
7593                    let off = m.offset_of_pos(row, col);
7594                    assert!(
7595                        m.is_stop(off),
7596                        "row {row} col {col} → {off} is not a stop in {src:?}"
7597                    );
7598                }
7599            }
7600        }
7601    }
7602
7603    /// `| Name | Qty |` with Name left-aligned and Qty right-aligned.
7604    const TABLE: &str = "| Name | Qty |\n|:-----|----:|\n| Pear | 3 |\n| Fig | 12 |\n";
7605
7606    #[test]
7607    fn a_table_renders_as_an_aligned_grid() {
7608        let text = rendered(&map(TABLE));
7609        assert_eq!(
7610            text,
7611            "┌──────┬─────┐\n\
7612             │ Name │ Qty │\n\
7613             ├──────┼─────┤\n\
7614             │ Pear │   3 │\n\
7615             │ Fig  │  12 │\n\
7616             └──────┴─────┘",
7617            "got:\n{text}"
7618        );
7619    }
7620
7621    #[test]
7622    fn table_columns_honour_their_alignment() {
7623        // Centre and default(left) come straight from twig's cell.alignment —
7624        // the delimiter row it's spelled in is consumed and has no node.
7625        let text = rendered(&map("| A | Bee |\n| --- | :---: |\n| x | y |\n"));
7626        assert!(text.contains("│ x │  y  │"), "centred column: {text:?}");
7627    }
7628
7629    #[test]
7630    fn table_borders_are_decoration_the_caret_never_lands_on() {
7631        let m = map(TABLE);
7632        // The top and header rules are whole decoration rows.
7633        for r in [0, 2] {
7634            assert!(m.rows[r].decoration, "row {r} should be a decoration rule");
7635            assert!(
7636                !m.rows[r].glyphs.iter().any(|g| g.stop),
7637                "row {r} has a stop"
7638            );
7639        }
7640        // The bottom border is the exception: no glyph of it is a stop, but
7641        // its end is the table's trailing caret home — the one place the caret
7642        // can stand past the last cell.
7643        let bottom = &m.rows[5];
7644        assert!(
7645            !bottom.decoration,
7646            "the bottom border holds the trailing stop"
7647        );
7648        assert!(
7649            !bottom.glyphs.iter().any(|g| g.stop),
7650            "the bottom border's glyphs are not stops"
7651        );
7652        assert!(m.is_stop(bottom.end_src), "the trailing stop is a stop");
7653        assert!(m.table_end_stop(bottom.end_src));
7654        assert_eq!(
7655            bottom.end_src,
7656            TABLE.trim_end_matches('\n').len(),
7657            "the trailing stop is the table's own end, before its newline"
7658        );
7659        assert!(
7660            !m.table_end_stop(TABLE.rfind("12").unwrap() + 2),
7661            "a cell's end is not the trailing stop"
7662        );
7663        // A content row's `│` and padding are decoration; only the cell text
7664        // and each cell's one end-stop are stops.
7665        let header = &m.rows[1];
7666        assert!(!header.decoration);
7667        for g in &header.glyphs {
7668            if g.ch == '│' {
7669                assert!(!g.stop, "a border is not a caret stop");
7670            }
7671        }
7672        let stops: String = header
7673            .glyphs
7674            .iter()
7675            .filter(|g| g.stop)
7676            .map(|g| g.ch)
7677            .collect();
7678        assert_eq!(stops, "Name Qty ", "cell text plus one end-stop space each");
7679    }
7680
7681    #[test]
7682    fn a_cell_maps_to_its_own_source_text() {
7683        let m = map(TABLE);
7684        // "Pear" starts at byte 32 in TABLE; the caret there draws on the 'P'.
7685        let pear = TABLE.find("Pear").unwrap();
7686        let (r, c) = m.pos_of_offset(pear);
7687        assert_eq!(m.rows[r].glyphs[c].ch, 'P');
7688        assert_eq!(m.offset_of_pos(r, c), pear, "round trips");
7689    }
7690
7691    #[test]
7692    fn a_wide_table_is_cut_to_fit_and_its_cells_wrap() {
7693        // Columns wider than the surface used to run off the right edge, where
7694        // nothing could reach them. They're cut to the budget instead, and the
7695        // text wraps down inside the column — the header rule stays put, and
7696        // an alignment holds on every line of a wrapped cell, not just the first.
7697        let src = "| Ingredient | Notes |\n|---|---:|\n\
7698                   | flour milled coarse | sift it twice |\n| salt | a pinch |\n";
7699        let mut ed = Editor::new_str(src, Format::Markdown).unwrap();
7700        let m = build_t(&ed.nodes().unwrap(), src, Some(30));
7701        let text = rendered(&m);
7702        assert_eq!(
7703            text,
7704            "┌──────────────┬─────────────┐\n\
7705             │ Ingredient   │       Notes │\n\
7706             ├──────────────┼─────────────┤\n\
7707             │ flour milled │     sift it │\n\
7708             │ coarse       │       twice │\n\
7709             │ salt         │     a pinch │\n\
7710             └──────────────┴─────────────┘",
7711            "got:\n{text}"
7712        );
7713        for (r, row) in m.rows.iter().enumerate() {
7714            assert!(
7715                row.glyphs.len() <= 30,
7716                "row {r} overflows: {}",
7717                row.glyphs.len()
7718            );
7719        }
7720    }
7721
7722    #[test]
7723    fn a_column_too_narrow_for_a_word_breaks_it_rather_than_spilling() {
7724        // A paragraph lets an overlong word trail off the end of the line; a
7725        // table column can't — a glyph past the border lands on the border.
7726        let src = "| A | B |\n|---|---|\n| antidisestablishmentarianism | x |\n";
7727        let mut ed = Editor::new_str(src, Format::Markdown).unwrap();
7728        let m = build_t(&ed.nodes().unwrap(), src, Some(20));
7729        for (r, row) in m.rows.iter().enumerate() {
7730            assert!(
7731                row.glyphs.len() <= 20,
7732                "row {r} overflows: {}",
7733                row.glyphs.len()
7734            );
7735        }
7736        // Broken across lines, but whole: every letter is still drawn, at its
7737        // own source byte, where the caret can reach it.
7738        let word = "antidisestablishmentarianism";
7739        let at = src.find(word).unwrap();
7740        for (i, ch) in word.char_indices() {
7741            assert!(
7742                m.rows
7743                    .iter()
7744                    .flat_map(|r| r.glyphs.iter())
7745                    .any(|g| g.stop && g.src == at + i && g.ch == ch),
7746                "{ch:?} at {} was lost to the break",
7747                at + i
7748            );
7749        }
7750    }
7751
7752    #[test]
7753    fn a_code_block_maps_each_line_to_its_own_source_text() {
7754        // Every glyph used to point at the block's start, which made the whole
7755        // block one offset — visible, but impossible to put a caret inside.
7756        let src = "```rust\nlet x = 1;\nfn f() {}\n```\n";
7757        let m = map(src);
7758        for row in &m.rows {
7759            for g in row.glyphs.iter().filter(|g| g.stop) {
7760                assert_eq!(
7761                    src[g.src..].chars().next(),
7762                    Some(g.ch),
7763                    "glyph {:?} at {} isn't the source byte it claims",
7764                    g.ch,
7765                    g.src
7766                );
7767            }
7768        }
7769    }
7770
7771    #[test]
7772    fn an_indented_code_block_maps_past_its_stripped_indent() {
7773        // twig strips the four-space indent, so `text` isn't a source slice and
7774        // the lines have to be re-found. Offsets land on the code, not the indent.
7775        let src = "    indented\n    code\n";
7776        let m = map(src);
7777        let stops: Vec<(char, usize)> = m
7778            .rows
7779            .iter()
7780            .flat_map(|r| r.glyphs.iter().filter(|g| g.stop).map(|g| (g.ch, g.src)))
7781            .collect();
7782        assert_eq!(
7783            stops[0],
7784            ('i', 4),
7785            "first line should start past the indent"
7786        );
7787        assert!(
7788            stops.contains(&('c', 17)),
7789            "second line misplaced: {stops:?}"
7790        );
7791    }
7792
7793    #[test]
7794    fn a_fenced_block_whose_code_echoes_its_info_string_maps_to_the_code() {
7795        // The one case that defeats a forward search: the opening fence
7796        // ```` ```rust ```` ends with the same text as the code under it.
7797        let src = "```rust\nrust\n```\n";
7798        let m = map(src);
7799        let first = m.rows[0].glyphs.iter().find(|g| g.stop).unwrap();
7800        assert_eq!(first.src, 8, "matched the info string, not the code");
7801    }
7802
7803    #[test]
7804    fn a_code_block_carries_no_gutter_and_is_published_as_a_row_span() {
7805        // The old `▏ ` gutter is gone: a code row is the block prefix (none, at
7806        // the top level) plus the code text, and the whole run is named in
7807        // `code_blocks` so a frontend can box it.
7808        let src = "para\n\n```\ncode\nlines\n```\n\nafter\n";
7809        let m = map(src);
7810        assert_eq!(m.code_blocks.len(), 1, "one code block");
7811        let span = m.code_blocks[0].rows_span.clone();
7812        let rows: Vec<String> = m.rows[span.clone()]
7813            .iter()
7814            .map(|r| r.glyphs.iter().map(|g| g.ch).collect())
7815            .collect();
7816        assert_eq!(rows, vec!["code".to_string(), "lines".to_string()]);
7817        assert!(!rendered(&m).contains('▏'), "gutter still drawn");
7818        assert!(
7819            m.rows[span].iter().all(|r| r.code),
7820            "every row in the span is flagged code"
7821        );
7822    }
7823
7824    #[test]
7825    fn an_empty_last_line_in_a_code_block_is_a_row_of_its_own() {
7826        // `trim_end_matches('\n')` cut the block's terminator *and* the newline
7827        // that spells a trailing empty line, so the row the Return had just made
7828        // never appeared and the caret on it fell through to the block below.
7829        // Every empty line is a row, wherever in the block it falls.
7830        let src = "prose\n\n```\nalpha\nbeta\n\n```\n\nafter\n";
7831        let m = map(src);
7832        let span = m.code_blocks[0].rows_span.clone();
7833        let rows: Vec<String> = m.rows[span.clone()]
7834            .iter()
7835            .map(|r| r.glyphs.iter().map(|g| g.ch).collect())
7836            .collect();
7837        assert_eq!(
7838            rows,
7839            vec!["alpha".to_string(), "beta".to_string(), String::new()],
7840            "the empty last line gets a row"
7841        );
7842        assert!(
7843            m.rows[span.clone()].iter().all(|r| r.code),
7844            "the empty row is flagged code like the rest of the block"
7845        );
7846        // And it is the *source's* empty line, not a coarse fallback to the
7847        // block start: the offset the caret resolves to is the one Return made.
7848        let empty = span.end - 1;
7849        assert_eq!(
7850            m.rows[empty].end_src,
7851            src.find("beta\n\n").unwrap() + "beta\n".len(),
7852            "the empty row maps to the line the Return opened"
7853        );
7854
7855        // Nothing is invented where there is no empty line, and a second one is
7856        // a second row.
7857        assert_eq!(
7858            map("```\nalpha\nbeta\n```\n").code_blocks[0]
7859                .rows_span
7860                .len(),
7861            2,
7862            "a block that ends at its last code line keeps two rows"
7863        );
7864        assert_eq!(
7865            map("```\nalpha\n\n\n```\n").code_blocks[0].rows_span.len(),
7866            3,
7867            "two trailing empty lines are two rows"
7868        );
7869    }
7870
7871    #[test]
7872    fn a_directive_container_is_tinted_and_labeled_on_its_first_row() {
7873        // diaryx's `:::vis{.public .family}` visibility block, and any other
7874        // `:::name{.class}` fenced div — core is agnostic of `name`.
7875        let src = ":::vis{.public .family}\nhello\n\nworld\n:::\nafter\n";
7876        let m = map_directives(src);
7877
7878        let content_rows: Vec<usize> = (0..m.rows.len()).filter(|&i| m.rows[i].directive).collect();
7879        assert!(!content_rows.is_empty(), "some row is flagged directive");
7880
7881        let after_rows: Vec<usize> = (0..m.rows.len())
7882            .filter(|&i| !content_rows.contains(&i) && !m.rows[i].glyphs.is_empty())
7883            .collect();
7884        assert!(
7885            after_rows.iter().all(|&i| !m.rows[i].directive),
7886            "content outside the fence isn't tinted"
7887        );
7888
7889        let labels: Vec<&str> = content_rows
7890            .iter()
7891            .filter_map(|&i| m.rows[i].directive_label.as_deref())
7892            .collect();
7893        assert_eq!(
7894            labels,
7895            vec!["public family"],
7896            "only the first row carries the label"
7897        );
7898
7899        assert_eq!(
7900            rendered(&m)
7901                .lines()
7902                .filter(|l| !l.is_empty())
7903                .collect::<Vec<_>>(),
7904            vec!["hello", "world", "after"],
7905            "fence markers don't leak into the rendered text"
7906        );
7907    }
7908
7909    #[test]
7910    fn a_bare_word_directive_is_labeled_same_as_dot_classes() {
7911        // diaryx_core::visibility's own `:::vis{public family}` — no leading
7912        // dots — is what apps/web's directive serializer and the native
7913        // publish-time filter both actually write today, distinct from twig's
7914        // `.class` convention. Both must label the same way so every existing
7915        // diaryx `:::vis{...}` block reads, not just newly dot-authored ones.
7916        let src = ":::vis{public family}\nhello\n:::\n";
7917        let m = map_directives(src);
7918        let label = m.rows.iter().find_map(|r| r.directive_label.clone());
7919        assert_eq!(label.as_deref(), Some("public family"));
7920    }
7921
7922    #[test]
7923    fn a_text_directive_keeps_its_paragraph_visible() {
7924        // Regression: an inline `:name[label]{…}` used to make its paragraph
7925        // fail the "all children inline" test, so the whole line was walked as
7926        // a container of blocks and rendered as empty rows with NO caret stops —
7927        // the text vanished from the editor and the caret couldn't enter it.
7928        // diaryx's inline `:vis[…]` is exactly this shape.
7929        let src = "Text with :abbr[HTML]{title=\"HyperText\"} inline.\n";
7930        let m = map_directives(src);
7931        assert_eq!(rendered(&m).trim_end(), "Text with HTML inline.");
7932        // Every character of the line is a caret home, markup excluded — the
7933        // label reads as ordinary text, the way a link's does.
7934        let stops: usize = m
7935            .rows
7936            .iter()
7937            .map(|r| r.glyphs.iter().filter(|g| g.stop).count())
7938            .sum();
7939        assert_eq!(stops, "Text with HTML inline.".chars().count());
7940        // It is inline, so it is not the container form's tinted panel.
7941        assert!(m.rows.iter().all(|r| !r.directive));
7942    }
7943
7944    #[test]
7945    fn a_text_directives_label_maps_to_its_true_source_bytes() {
7946        // Regression (needs twig-doc >= 2.5.0): twig parses a `[label]` as a
7947        // detached slice, and until it rebased the enclosing scan's segments
7948        // onto it every node inside the label reported a span of `(0,0)`. Read
7949        // by anything that trusts a span that means "byte 0", so the label's
7950        // glyphs mapped to the START OF THE DOCUMENT — a click on the label put
7951        // the caret at the top of the file, its stops collided with the real
7952        // first line's, and an edit there landed on the wrong bytes entirely.
7953        //
7954        // The sibling test `a_text_directive_keeps_its_paragraph_visible` only
7955        // counts stops, which is exactly why this went unnoticed: the right
7956        // NUMBER of stops at completely wrong offsets.
7957        let src = "x :abbr[HTML]{title=\"y\"} z\n";
7958        let m = map_directives(src);
7959        let stops: Vec<(char, usize)> = m
7960            .rows
7961            .iter()
7962            .flat_map(|r| &r.glyphs)
7963            .filter(|g| g.stop)
7964            .map(|g| (g.ch, g.src))
7965            .collect();
7966        // `HTML` sits at 8..12. The name, brackets and `{…}` are hidden markup
7967        // the caret steps over, so the line's stops run 0, 1, 8..12, then 24.
7968        assert_eq!(
7969            stops,
7970            [
7971                ('x', 0),
7972                (' ', 1),
7973                ('H', 8),
7974                ('T', 9),
7975                ('M', 10),
7976                ('L', 11),
7977                (' ', 24),
7978                ('z', 25)
7979            ]
7980        );
7981    }
7982
7983    #[test]
7984    fn every_glyph_in_a_directive_label_points_at_its_source_byte() {
7985        // The `every_glyph_points_at_its_source_byte` invariant, extended over
7986        // directive labels now that their offsets are real. Nested markup is
7987        // included: its delimiters are hidden, so the visible glyphs must skip
7988        // them and still name their own bytes.
7989        let src = "x :abbr[a *b* c] y and :vis[family only] z\n";
7990        let m = map_directives(src);
7991        for g in m.rows.iter().flat_map(|r| &r.glyphs).filter(|g| g.stop) {
7992            let at = src[g.src..].chars().next();
7993            assert_eq!(
7994                at,
7995                Some(g.ch),
7996                "glyph {:?} claims byte {}, which is {at:?}",
7997                g.ch,
7998                g.src
7999            );
8000        }
8001        assert_eq!(rendered(&m).trim_end(), "x a b c y and family only z");
8002    }
8003
8004    #[test]
8005    fn a_directive_labels_nested_emphasis_keeps_both_its_style_and_its_offsets() {
8006        let src = "x :abbr[a *b* c] y\n";
8007        let m = map_directives(src);
8008        let b = m
8009            .rows
8010            .iter()
8011            .flat_map(|r| &r.glyphs)
8012            .find(|g| g.ch == 'b')
8013            .expect("the emphasised char");
8014        assert!(b.style.italic, "the label's *b* lost its emphasis");
8015        assert_eq!(b.src, 11, "the label's *b* lost its source byte");
8016    }
8017
8018    #[test]
8019    fn a_bare_colon_word_renders_as_the_prose_it_almost_always_is() {
8020        // Regression: twig matches a colon followed by any letter-led word, so
8021        // ordinary prose is full of "text directives" nobody meant to write.
8022        // With no `[label]` there are no children, and the arm recursed into
8023        // them — rendering *nothing*. The word vanished from the document with
8024        // no caret stop left behind, so it could not even be deleted.
8025        for src in ["a :word b\n", "note :see below\n", ":smile: hi\n"] {
8026            let m = map_directives(src);
8027            assert_eq!(
8028                rendered(&m).trim_end(),
8029                src.trim_end(),
8030                "prose was eaten: {src:?}"
8031            );
8032        }
8033    }
8034
8035    #[test]
8036    fn a_bare_colon_word_keeps_every_byte_a_caret_stop() {
8037        let src = "a :word b\n";
8038        let m = map_directives(src);
8039        // Nothing here is markup, so nothing is hidden: each byte maps to
8040        // itself and can be stood on, which is what makes the colon deletable.
8041        let stops: Vec<(char, usize)> = m
8042            .rows
8043            .iter()
8044            .flat_map(|r| &r.glyphs)
8045            .filter(|g| g.stop)
8046            .map(|g| (g.ch, g.src))
8047            .collect();
8048        assert_eq!(
8049            stops,
8050            "a :word b"
8051                .chars()
8052                .enumerate()
8053                .map(|(i, c)| (c, i))
8054                .collect::<Vec<_>>()
8055        );
8056    }
8057
8058    #[test]
8059    fn an_attribute_bearing_text_directive_draws_a_chip() {
8060        // `{…}` is deliberate in a way a bare colon is not — diaryx writes
8061        // `:vis{.family}` inline — so this one reads as an embed, on the same
8062        // `⧉ label` recipe the leaf form's placeholder row uses.
8063        // Both attribute conventions label it: twig's dot-prefixed classes and
8064        // the bare pandoc-style words diaryx also writes.
8065        for src in ["a :vis{.family} b\n", "a :vis{family} b\n"] {
8066            let m = map_directives(src);
8067            assert_eq!(rendered(&m).trim_end(), "a ⧉ vis family b", "{src:?}");
8068        }
8069        // A `key=value` attr is configuration, not a name, so it adds nothing.
8070        let m = map_directives("a :foo{title=\"x\"} b\n");
8071        assert_eq!(rendered(&m).trim_end(), "a ⧉ foo b");
8072    }
8073
8074    #[test]
8075    fn a_directive_chip_is_one_atomic_caret_stop_at_its_own_offset() {
8076        let src = "a :vis{.family} b\n";
8077        let m = map_directives(src);
8078        let stops: Vec<usize> = m
8079            .rows
8080            .iter()
8081            .flat_map(|r| &r.glyphs)
8082            .filter(|g| g.stop)
8083            .map(|g| g.src)
8084            .collect();
8085        // The chip contributes exactly one stop, at the directive's start (2),
8086        // so the caret steps over it whole instead of walking hidden markup a
8087        // byte at a time. `{.family}`'s bytes (3..15) are never stood on.
8088        assert_eq!(stops, [0, 1, 2, 15, 16]);
8089    }
8090
8091    #[test]
8092    fn a_paragraph_holding_only_a_chip_is_still_navigable() {
8093        // With no stop of its own the row would be unreachable — the caret
8094        // could never be put on the line to edit or delete the directive.
8095        let m = map_directives(":vis{.family}\n");
8096        assert!(
8097            m.row_is_navigable(0),
8098            "a chip-only paragraph has no caret home"
8099        );
8100        assert_eq!(
8101            m.offset_of_pos(0, 0),
8102            0,
8103            "its caret home isn't the directive's start"
8104        );
8105    }
8106
8107    #[test]
8108    fn a_ratio_or_a_clock_time_is_never_a_directive() {
8109        // twig requires a letter after the colon, so these stay prose — the
8110        // verbatim arm must not be reached for them at all.
8111        let src = "ratio 3:4 and 10:30\n";
8112        assert_eq!(
8113            rendered(&map_directives(src)).trim_end(),
8114            "ratio 3:4 and 10:30"
8115        );
8116    }
8117
8118    #[test]
8119    fn a_leaf_directive_is_a_placeholder_row_with_its_attrs_published() {
8120        // `::name{…}` is a standalone block with no body — an embed, a table of
8121        // contents. It used to emit no rows at all: invisible, no caret home,
8122        // vertical motion crossing a void. Now it draws the image recipe's
8123        // placeholder and publishes what the host app needs to paint the real
8124        // thing.
8125        let src = "before\n\n::embed{src=\"demo.html\" height=\"400\"}\n\nafter\n";
8126        let m = map_directives(src);
8127
8128        let row = m
8129            .rows
8130            .iter()
8131            .position(|r| r.leaf_directive.is_some())
8132            .expect("a placeholder row");
8133        assert_eq!(
8134            m.rows[row].glyphs.iter().map(|g| g.ch).collect::<String>(),
8135            "⧉ embed"
8136        );
8137        assert!(
8138            m.rows[row].glyphs.iter().any(|g| g.stop),
8139            "the caret can land on it"
8140        );
8141        assert!(
8142            m.rows[row].directive,
8143            "a frontend frames it like the container form"
8144        );
8145
8146        assert_eq!(m.directives.len(), 1);
8147        let info = &m.directives[0];
8148        assert_eq!(info.name, "embed");
8149        assert_eq!(info.rows_span, row..row + 1);
8150        assert_eq!(info.attr("src"), Some("demo.html"));
8151        assert_eq!(info.attr("height"), Some("400"));
8152        assert_eq!(info.attr("nope"), None);
8153        // The prose around it is untouched.
8154        assert!(rendered(&m).contains("before") && rendered(&m).contains("after"));
8155    }
8156
8157    #[test]
8158    fn a_leaf_directive_shows_its_label_and_honours_its_prefix() {
8159        // A `[label]` names the placeholder (the way an image's alt does), and a
8160        // quoted directive keeps the quote's gutter — it is a block like any
8161        // other, not a special case that escapes its container.
8162        let m = map_directives("::embed[Audience demo]{src=\"demo.html\"}\n");
8163        assert_eq!(rendered(&m).trim_end(), "⧉ Audience demo");
8164        assert_eq!(m.directives[0].label, "Audience demo");
8165
8166        let quoted = map_directives("> ::embed{src=\"x.html\"}\n");
8167        assert_eq!(rendered(&quoted).trim_end(), "│ ⧉ embed");
8168        assert_eq!(quoted.directives[0].name, "embed");
8169    }
8170
8171    #[test]
8172    fn a_container_directive_is_still_a_panel_not_a_placeholder() {
8173        // The three forms must not bleed into each other: only the leaf form is
8174        // a placeholder, and only the container form tints the blocks it wraps.
8175        let m = map_directives(":::note{.warning}\nBody\n:::\n");
8176        assert!(
8177            m.directives.is_empty(),
8178            "a container publishes no placeholder"
8179        );
8180        assert!(m.rows.iter().all(|r| r.leaf_directive.is_none()));
8181        assert_eq!(rendered(&m).trim_end(), "Body");
8182        assert!(
8183            m.rows
8184                .iter()
8185                .any(|r| r.directive && r.directive_label.as_deref() == Some("warning"))
8186        );
8187    }
8188
8189    /// A production-path build with both extensions on — the only way to put a
8190    /// promoted HTML element and a directive in one document, which is what the
8191    /// `container` kind made necessary to tell apart. Returns the whole `Doc`
8192    /// because [`VisualMap`] is not `Clone`; read `doc.vmap`.
8193    fn doc_built(src: &str) -> crate::Doc {
8194        let mut doc = crate::Doc::from_source(src.to_string(), Format::Markdown).unwrap();
8195        doc.build_visual(80);
8196        doc
8197    }
8198
8199    /// Every `container` node in `src`, parsed the way production does (both
8200    /// extensions on), paired with what [`container_is_directive`] makes of it.
8201    fn containers(src: &str) -> Vec<(String, bool, Option<DirectiveForm>)> {
8202        let mut ed = Editor::new_ext(
8203            src.as_bytes(),
8204            Format::Markdown,
8205            twig::MarkdownExtensions {
8206                directives: true,
8207                html_elements: true,
8208                ..Default::default()
8209            },
8210        )
8211        .unwrap();
8212        ed.nodes()
8213            .unwrap()
8214            .iter()
8215            .filter(|n| n.kind == Kind::Container)
8216            .map(|n| {
8217                (
8218                    n.name.clone().unwrap_or_default(),
8219                    container_is_directive(n),
8220                    n.directive_form,
8221                )
8222            })
8223            .collect()
8224    }
8225
8226    #[test]
8227    fn a_directive_and_an_html_element_are_told_apart_by_spelling_not_by_form() {
8228        // twig 2.8 folded `div`/`span`/`directive`/`element` into one `container`
8229        // kind. `directive_form` reads as though it separates them and does not:
8230        // a block-level `<div>` reports `Some(DirectiveForm::Container)` exactly
8231        // as a `:::note` does. Trusting it would draw directive chrome — a tinted
8232        // panel, a `.class` audience label — on every pasted Slack/Docs div.
8233        for (src, name, want) in [
8234            (":::note{.a}\nbody\n:::\n", "note", true),
8235            ("::embed{src=x}\n", "embed", true),
8236            ("a :vis[hi]{.b} b\n", "vis", true),
8237            ("<div class=\"x\">\nhi\n</div>\n", "div", false),
8238            ("<video src=\"v.mp4\" controls></video>\n", "video", false),
8239            ("<audio src=\"a.mp3\" controls></audio>\n", "audio", false),
8240            ("<figure>\n\nhi\n\n</figure>\n", "figure", false),
8241            // The `:` in an attribute must not read as a directive opener: the
8242            // `<` of the tag comes first, and first one wins.
8243            (
8244                "<video src=\"http://x.test/v.mp4\" controls></video>\n",
8245                "video",
8246                false,
8247            ),
8248            (
8249                "<source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\">\n",
8250                "source",
8251                false,
8252            ),
8253        ] {
8254            let found = containers(src);
8255            let hit = found.iter().find(|(n, ..)| n == name);
8256            let Some((_, is_directive, form)) = hit else {
8257                panic!("no `{name}` container in {src:?} — found {found:?}");
8258            };
8259            assert_eq!(*is_directive, want, "{name} in {src:?} (form was {form:?})");
8260        }
8261
8262        // And the reason this can't just read the field: for the one collision
8263        // that matters, the field says the same thing for both.
8264        let div = containers("<div class=\"x\">\nhi\n</div>\n");
8265        let note = containers(":::note{.a}\nbody\n:::\n");
8266        assert_eq!(
8267            div[0].2, note[0].2,
8268            "if these ever differ, `directive_form` became usable and this rule can go"
8269        );
8270    }
8271
8272    #[test]
8273    fn a_directive_nested_in_a_quote_or_list_is_still_a_directive() {
8274        // A container's span opens with its *block prefix*, not its own markup —
8275        // `> ::embed{…}` starts at the `>`. Reading only the first byte to tell a
8276        // directive from an element (both `container` since 2.8) therefore misses
8277        // every nested one, and the placeholder silently renders as nothing.
8278        for (src, ctx) in [
8279            ("> ::embed{src=\"x\"}\n", "quoted"),
8280            ("- ::embed{src=\"x\"}\n", "listed"),
8281            (">> ::embed{src=\"x\"}\n", "twice quoted"),
8282        ] {
8283            let m = map_directives(src);
8284            assert_eq!(m.directives.len(), 1, "{ctx} directive was lost");
8285            assert_eq!(m.directives[0].name, "embed", "{ctx}");
8286        }
8287    }
8288
8289    #[test]
8290    fn a_video_is_still_media_and_not_a_directive() {
8291        // The other side of the same coin: `<video>` is a `container` too, and
8292        // must reach `block_media` rather than the directive arms.
8293        let doc = doc_built("<video src=\"clip.mp4\" controls></video>\n");
8294        assert_eq!(doc.vmap.media.len(), 1, "the video is block media");
8295        assert!(
8296            doc.vmap.rows.iter().all(|r| !r.directive),
8297            "the video drew directive chrome"
8298        );
8299    }
8300
8301    #[test]
8302    fn a_directive_needs_the_extension_flag() {
8303        // `map` (twig's default extensions) leaves `directives` off — the fence
8304        // renders as literal paragraph text, same as any other unrecognized
8305        // punctuation, never corrupting or panicking.
8306        let src = ":::vis{.public}\nhello\n:::\n";
8307        let m = map(src);
8308        assert!(m.rows.iter().all(|r| !r.directive));
8309        assert!(rendered(&m).contains(":::vis{.public}"));
8310    }
8311
8312    #[test]
8313    fn a_footnote_reference_keeps_its_paragraph_visible() {
8314        // Regression: `footnote_reference` was in neither `is_inline_kind` nor
8315        // the inline walker, so a paragraph carrying one failed the "all children
8316        // inline" test, was walked as a container of blocks, and rendered as
8317        // empty rows with no caret stop anywhere — the whole line vanished.
8318        let src = "A claim[^1] and more.\n";
8319        let m = map(src);
8320        assert_eq!(rendered(&m).trim_end(), "A claim[1] and more.");
8321        // The `^` is spelling, not text: hidden the way a link's `](dest)` is.
8322        assert!(!rendered(&m).contains('^'));
8323    }
8324
8325    #[test]
8326    fn a_footnote_reference_is_raised_and_the_prose_around_it_is_not() {
8327        // What makes `[1]` read as a reference rather than as bracketed text.
8328        // The brackets ride with the label: the chip is one raised mark.
8329        let m = map("A claim[^1] and more.\n");
8330        assert_eq!(baselines_of(&m, '1'), vec![Baseline::Super]);
8331        assert_eq!(baselines_of(&m, '['), vec![Baseline::Super]);
8332        assert_eq!(baselines_of(&m, ']'), vec![Baseline::Super]);
8333        assert_eq!(baselines_of(&m, 'A'), vec![Baseline::Normal]);
8334    }
8335
8336    #[test]
8337    fn a_footnote_reference_keeps_the_link_role_it_had() {
8338        // The raised baseline is added to the role, not swapped for it: every
8339        // frontend already paints `Role::Link`, and a reference is one.
8340        let m = map("A claim[^1].\n");
8341        let label = m
8342            .rows
8343            .iter()
8344            .flat_map(|r| &r.glyphs)
8345            .find(|g| g.ch == '1')
8346            .unwrap();
8347        assert_eq!(label.style.role, Role::Link);
8348        assert_eq!(label.style.baseline, Baseline::Super);
8349    }
8350
8351    /// The [`Role`] of the first glyph spelling `ch` — how a test reads one
8352    /// run's styling off a map without caring which row it landed on.
8353    fn role_of(m: &VisualMap, ch: char) -> Role {
8354        m.rows
8355            .iter()
8356            .flat_map(|r| r.glyphs.iter())
8357            .find(|g| g.ch == ch)
8358            .unwrap_or_else(|| panic!("no glyph spelling {ch:?}"))
8359            .style
8360            .role
8361    }
8362
8363    #[test]
8364    fn a_markdown_highlight_is_a_mark_and_a_coloured_one_names_its_colour() {
8365        // twig 3.3's `highlight`/`highlight_colors`, which `parse_extensions`
8366        // turns on for every leaf document: `==text==` is a `mark` in Markdown
8367        // and not the literal `==` it used to be, and `==🔴 text==` is one
8368        // carrying a colour.
8369        //
8370        // `doc_built` rather than `map`, deliberately — the extensions are
8371        // leaf's choice, not twig's default, so a test that parsed bare
8372        // Markdown here would be testing a document leaf never builds.
8373        let doc = doc_built("Plain ==yes== and ==🔴 red== ok\n");
8374        assert_eq!(role_of(&doc.vmap, 'y'), Role::Mark(None));
8375        assert_eq!(
8376            role_of(&doc.vmap, 'r'),
8377            Role::Mark(Some(MarkColor::Red)),
8378            "the `data-color` twig stripped the emoji into"
8379        );
8380        assert_eq!(role_of(&doc.vmap, 'P'), Role::Body);
8381    }
8382
8383    #[test]
8384    fn the_emoji_that_named_a_highlight_is_markup_and_never_drawn() {
8385        // The colour is *spelling*: twig strips the emoji out of the mark's
8386        // content, so the reader sees the words and the wash, never the circle.
8387        // Drawing it would put a character in the rendered text that the author
8388        // wrote as syntax — the same mistake as drawing an emphasis's `*`.
8389        let doc = doc_built("Plain ==yes== and ==🔴 red== ok\n");
8390        let drawn: String = doc.vmap.rows[0].glyphs.iter().map(|g| g.ch).collect();
8391        assert_eq!(drawn, "Plain yes and red ok");
8392    }
8393
8394    #[test]
8395    fn a_superscript_and_a_subscript_sit_off_the_baseline() {
8396        // Regression: both rendered flat, so the toolbar's superscript button
8397        // produced markup that looked exactly like the text around it.
8398        let m = map_djot("H~2~O and x^2^\n");
8399        assert_eq!(baselines_of(&m, '2'), vec![Baseline::Sub, Baseline::Super]);
8400        assert_eq!(baselines_of(&m, 'H'), vec![Baseline::Normal]);
8401        assert_eq!(baselines_of(&m, 'O'), vec![Baseline::Normal]);
8402    }
8403
8404    #[test]
8405    fn a_raised_glyph_keeps_the_style_it_was_raised_out_of() {
8406        // Why this is a `Baseline` and not a `Role`: raising a glyph says where
8407        // it sits, and must not cost it what it already was.
8408        let m = map_djot("# Heading x^2^\n");
8409        let two = m
8410            .rows
8411            .iter()
8412            .flat_map(|r| &r.glyphs)
8413            .find(|g| g.ch == '2')
8414            .unwrap();
8415        assert_eq!(two.style.baseline, Baseline::Super);
8416        assert_eq!(two.style.role, Role::Heading(1), "still heading text");
8417    }
8418
8419    #[test]
8420    fn a_footnote_references_brackets_are_decoration_and_only_its_label_is_a_stop() {
8421        let src = "see[^note] here\n";
8422        let m = map(src);
8423        // `[^note]` spans 3..10, its label `note` 5..9. The caret walks the
8424        // label; the brackets are drawn but never stood on, as a table's are,
8425        // and the `[^`/`]` bytes are stepped over like any hidden delimiter.
8426        let stops: Vec<usize> = m
8427            .rows
8428            .iter()
8429            .flat_map(|r| &r.glyphs)
8430            .filter(|g| g.stop)
8431            .map(|g| g.src)
8432            .collect();
8433        for off in 5..9 {
8434            assert!(
8435                stops.contains(&off),
8436                "label byte {off} isn't a caret stop: {stops:?}"
8437            );
8438        }
8439        for off in [3usize, 4, 9] {
8440            assert!(
8441                !stops.contains(&off),
8442                "delimiter byte {off} is a caret stop: {stops:?}"
8443            );
8444        }
8445    }
8446
8447    #[test]
8448    fn a_task_item_draws_its_box_where_the_bullet_would_be() {
8449        // Regression: the `[ ] ` is markup twig consumes — the item's paragraph
8450        // content starts past it — so a task item used to render as `• todo`,
8451        // identical to a plain bullet and with no way to see it was ticked.
8452        let m = map("- [ ] todo\n- [x] done\n- plain\n");
8453        assert_eq!(rendered(&m), "☐ todo\n☑ done\n• plain");
8454
8455        // The tick rides the item's first row, for a GUI that paints its own box.
8456        let ticks: Vec<Option<bool>> = m.rows.iter().map(|r| r.task).collect();
8457        assert_eq!(ticks, [Some(false), Some(true), None]);
8458    }
8459
8460    #[test]
8461    fn a_task_items_box_survives_a_wrap_and_marks_only_the_first_row() {
8462        let m = map_at(
8463            "- [x] a much longer task that has to wrap somewhere\n",
8464            Some(20),
8465        );
8466        assert!(m.rows.len() > 1, "the item should wrap: {:?}", rendered(&m));
8467        assert_eq!(m.rows[0].task, Some(true));
8468        assert!(
8469            m.rows[1..].iter().all(|r| r.task.is_none()),
8470            "only the first row"
8471        );
8472        // The continuation lines hang under the box, not under column zero.
8473        assert!(
8474            rendered(&m)
8475                .lines()
8476                .nth(1)
8477                .is_some_and(|l| l.starts_with("  "))
8478        );
8479    }
8480
8481    #[test]
8482    fn a_bracket_in_an_items_prose_is_not_a_checkbox() {
8483        // `task_checked` finds the box past the list marker; a plain item whose
8484        // text merely contains a bracket has none, and must keep its bullet.
8485        let m = map("- see [1] below\n");
8486        assert_eq!(rendered(&m), "• see [1] below");
8487        assert_eq!(m.rows[0].task, None);
8488    }
8489
8490    #[test]
8491    fn a_footnote_definition_renders_where_it_was_written() {
8492        // Regression: twig parses `[^1]: …` as a root *beside* `doc` — not a
8493        // child of it — so the walk from `doc` never reached one and every byte
8494        // of the note's body rendered as nothing at all.
8495        let src = "A claim[^1].\n\n[^1]: The note body.\n\nAfter.\n";
8496        let m = map(src);
8497        let text = rendered(&m);
8498        assert!(
8499            text.contains("The note body."),
8500            "the note body is invisible: {text:?}"
8501        );
8502        // In source order — between the paragraph that cites it and the one
8503        // after — not hoisted to the end, and marked to match its reference.
8504        let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
8505        assert_eq!(lines, ["A claim[1].", "[1] The note body.", "After."]);
8506    }
8507
8508    #[test]
8509    fn a_footnote_definitions_body_maps_to_its_own_source_bytes() {
8510        let src = "x[^a].\n\n[^a]: body\n";
8511        let m = map(src);
8512        // `body` sits at 14..18. Its glyphs must map there — a marker that ate
8513        // the offsets would put the caret in the wrong place on every click.
8514        let body: Vec<(char, usize)> = m
8515            .rows
8516            .iter()
8517            .flat_map(|r| &r.glyphs)
8518            .filter(|g| g.stop && g.src >= 14)
8519            .map(|g| (g.ch, g.src))
8520            .collect();
8521        assert_eq!(body, [('b', 14), ('o', 15), ('d', 16), ('y', 17)]);
8522    }
8523
8524    #[test]
8525    fn an_empty_footnote_definition_still_shows_its_marker() {
8526        // The instant `[^1]: ` has been typed and nothing after it. `blocks`
8527        // renders no child, so without the explicit marker row the definition
8528        // wouldn't appear at all until something was typed into it.
8529        let src = "x[^1]\n\n[^1]:\n";
8530        let m = map(src);
8531        assert!(
8532            rendered(&m).contains("[1] "),
8533            "no marker row: {:?}",
8534            rendered(&m)
8535        );
8536    }
8537
8538    #[test]
8539    fn a_footnote_definition_wearing_a_long_label_indents_its_wrapped_body() {
8540        let src = "x[^src]\n\n[^src]: one two three four five six seven\n";
8541        let m = map_at(src, Some(24));
8542        let text = rendered(&m);
8543        let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
8544        // Continuation lines hang under the marker, as a list item's do — the
8545        // indent is the marker's own width, not a fixed one.
8546        assert_eq!(lines[1].trim_end(), "[src] one two three four");
8547        assert!(
8548            lines[2].starts_with("      "),
8549            "body doesn't hang: {:?}",
8550            lines[2]
8551        );
8552        assert_eq!(lines[2].trim(), "five six seven");
8553    }
8554
8555    #[test]
8556    fn a_code_block_leaves_exactly_one_blank_row_below_it() {
8557        // The closing fence line used to be miscounted as a blank separator,
8558        // opening a phantom second gap under the block. One block boundary is
8559        // one blank row, code block or not.
8560        let src = "para\n\n```\ncode\n```\n\nafter\n";
8561        let m = map(src);
8562        let code_end = m.code_blocks[0].rows_span.end;
8563        let after = m
8564            .rows
8565            .iter()
8566            .position(|r| r.glyphs.iter().map(|g| g.ch).collect::<String>() == "after")
8567            .unwrap();
8568        assert_eq!(
8569            after - code_end,
8570            1,
8571            "exactly one row between code and 'after'"
8572        );
8573    }
8574
8575    #[test]
8576    fn a_fenced_block_publishes_its_language_on_its_code_block() {
8577        // The info string becomes the block's label; a bare fence and an indented
8578        // block carry none.
8579        assert_eq!(
8580            map("```rust\nlet x = 1;\n```\n").code_blocks[0]
8581                .lang
8582                .as_deref(),
8583            Some("rust")
8584        );
8585        assert_eq!(map("```\nplain\n```\n").code_blocks[0].lang, None);
8586        assert_eq!(map("    indented\n").code_blocks[0].lang, None);
8587    }
8588
8589    fn lang_of(src: &str) -> Option<String> {
8590        map(src).code_blocks[0].lang.clone()
8591    }
8592
8593    #[test]
8594    fn a_fence_in_a_quote_has_its_language() {
8595        // The block's span starts at the line, `> ` and all; the fence is past
8596        // the quote marker, and so is its info string.
8597        let src = "> ```rust\n> let x = 1;\n> ```\n";
8598        assert_eq!(lang_of(src).as_deref(), Some("rust"));
8599        let info = code_info_span(src, 0).unwrap();
8600        assert_eq!(&src[info], "rust", "the info string's exact bytes");
8601        assert_eq!(
8602            lang_of("> > ~~~ py\n> > x\n> > ~~~\n").as_deref(),
8603            Some("py")
8604        );
8605    }
8606
8607    #[test]
8608    fn a_fence_in_a_list_item_has_its_language() {
8609        // On the item's own line, past its marker…
8610        assert_eq!(
8611            lang_of("- ```rust\n  let x = 1;\n  ```\n").as_deref(),
8612            Some("rust")
8613        );
8614        assert_eq!(
8615            lang_of("10. ```rust\n    let x = 1;\n    ```\n").as_deref(),
8616            Some("rust")
8617        );
8618        // …and on a line of its own inside the item, where the indent is the
8619        // item's content column and not the fence's.
8620        assert_eq!(
8621            lang_of("10. a\n\n    ```rust\n    let x = 1;\n    ```\n").as_deref(),
8622            Some("rust")
8623        );
8624        assert_eq!(
8625            lang_of("- a\n  - b\n\n    ```rust\n    x\n    ```\n").as_deref(),
8626            Some("rust")
8627        );
8628        // In a list in a quote.
8629        assert_eq!(
8630            lang_of("> - a\n>\n>   ```rust\n>   x\n>   ```\n").as_deref(),
8631            Some("rust")
8632        );
8633    }
8634
8635    #[test]
8636    fn an_indented_block_in_a_list_item_still_has_no_language() {
8637        // Four spaces past the item's content column is an indented code
8638        // block, whatever its text looks like — the allowance is measured from
8639        // the item, not dropped.
8640        let src = "- a\n\n      ```rust\n";
8641        assert_eq!(map(src).code_blocks.len(), 1);
8642        assert_eq!(lang_of(src), None);
8643        assert_eq!(lang_of("- a\n\n      indented\n"), None);
8644    }
8645
8646    /// The token every glyph spelling `ch` carries, in row order — how a test
8647    /// reads a block's highlighting off the map.
8648    fn tokens_of(m: &VisualMap, ch: char) -> Vec<Option<Token>> {
8649        m.rows
8650            .iter()
8651            .flat_map(|r| r.glyphs.iter())
8652            .filter(|g| g.ch == ch)
8653            .map(|g| g.style.token)
8654            .collect()
8655    }
8656
8657    #[cfg(feature = "syntax")]
8658    #[test]
8659    fn a_fenced_block_in_a_known_language_carries_tokens() {
8660        // `let` is a keyword, the string literal a string, and the plain
8661        // identifier `x` nothing at all — it draws in the code colour. Every
8662        // glyph is still `Role::Code`: a token is beside the role, not instead.
8663        let m = map("```rust\nlet x = \"s\";\n```\n");
8664        assert_eq!(tokens_of(&m, 'l'), vec![Some(Token::Keyword)]);
8665        assert_eq!(tokens_of(&m, 'x'), vec![None]);
8666        assert_eq!(tokens_of(&m, '"'), vec![Some(Token::String); 2]);
8667        assert!(
8668            m.rows
8669                .iter()
8670                .filter(|r| r.code)
8671                .flat_map(|r| r.glyphs.iter())
8672                .all(|g| g.style.role == Role::Code),
8673            "a token replaced the code role"
8674        );
8675    }
8676
8677    #[cfg(feature = "syntax")]
8678    #[test]
8679    fn a_token_changes_nothing_about_where_a_glyph_is() {
8680        // The same block with and without a language it can be highlighted in
8681        // lays out identically: same rows, same offsets, same stops. Only the
8682        // token differs, so the caret walks a highlighted block as it walked an
8683        // unhighlighted one.
8684        let hl = map("```rust\nlet x = 1; // c\nfn f() {}\n```\n");
8685        let plain = map("```text\nlet x = 1; // c\nfn f() {}\n```\n");
8686        assert_eq!(hl.rows.len(), plain.rows.len());
8687        for (a, b) in hl.rows.iter().zip(&plain.rows) {
8688            assert_eq!(a.end_src, b.end_src);
8689            assert_eq!(a.glyphs.len(), b.glyphs.len());
8690            for (ga, gb) in a.glyphs.iter().zip(&b.glyphs) {
8691                assert_eq!((ga.ch, ga.src, ga.stop), (gb.ch, gb.src, gb.stop));
8692                assert_eq!(ga.style.token(None), gb.style);
8693            }
8694        }
8695        assert!(tokens_of(&hl, 'l').iter().any(Option::is_some));
8696        assert!(tokens_of(&plain, 'l').iter().all(Option::is_none));
8697    }
8698
8699    #[test]
8700    fn a_block_with_no_language_to_highlight_in_carries_no_tokens() {
8701        // A bare fence, an indented block, a fence in a language no grammar
8702        // covers, and inline code all draw as plain code — and so does a
8703        // `rust` fence when the `syntax` feature is off.
8704        for src in [
8705            "```\nlet x = 1;\n```\n",
8706            "    let x = 1;\n",
8707            "```no-such-language\nlet x = 1;\n```\n",
8708            "a `let x` b\n",
8709        ] {
8710            assert!(
8711                tokens_of(&map(src), 'l').iter().all(Option::is_none),
8712                "{src:?} was highlighted"
8713            );
8714        }
8715        #[cfg(not(feature = "syntax"))]
8716        assert!(
8717            tokens_of(&map("```rust\nlet x = 1;\n```\n"), 'l')
8718                .iter()
8719                .all(Option::is_none)
8720        );
8721    }
8722
8723    #[test]
8724    fn inline_code_is_not_a_code_block() {
8725        // A `code` span inside prose is styled by role, not boxed: it's part of a
8726        // normal paragraph row, so it names no `code_blocks` entry.
8727        let m = map("a `snippet` b\n");
8728        assert!(m.code_blocks.is_empty(), "inline code wrongly boxed");
8729        assert!(
8730            m.rows.iter().all(|r| !r.code),
8731            "inline code flagged a code row"
8732        );
8733    }
8734
8735    #[test]
8736    fn caret_steps_over_hidden_delimiters() {
8737        // "a **bold** c": bytes 8,9 are the closing ** — no glyph. Moving right
8738        // from 'd' (src 7) lands on the space before 'c' (src 10), not inside **.
8739        let m = map("a **bold** c\n");
8740        let (r, c) = m.pos_of_offset(7);
8741        assert_eq!(m.offset_of_pos(r, c + 1), 10);
8742    }
8743
8744    // ── the structural view of a table ───────────────────────────────────────
8745
8746    #[test]
8747    fn a_table_is_published_structurally_beside_its_picture() {
8748        let m = map(TABLE);
8749        let t = &m.tables[0];
8750        let cell = |r: usize, c: usize| -> String {
8751            t.grid[r].cells[c].glyphs.iter().map(|g| g.ch).collect()
8752        };
8753        assert_eq!(t.grid.len(), 3, "head + two body rows");
8754        assert_eq!(
8755            (cell(0, 0), cell(0, 1), cell(1, 0), cell(2, 1)),
8756            ("Name".into(), "Qty".into(), "Pear".into(), "12".into())
8757        );
8758        assert_eq!(
8759            t.grid.iter().map(|r| r.head).collect::<Vec<_>>(),
8760            [true, false, false]
8761        );
8762        // The alignment the delimiter row spelled, carried per cell — the only
8763        // place it survives, since the parser consumes that row.
8764        assert!(matches!(t.grid[1].cells[0].align, Alignment::Left));
8765        assert!(matches!(t.grid[1].cells[1].align, Alignment::Right));
8766    }
8767
8768    #[test]
8769    fn a_block_media_is_published_structurally_beside_its_placeholder() {
8770        let m = map("intro\n\n![a cat](img/cat.png)\n\nend\n");
8771        assert_eq!(m.media.len(), 1, "one block image");
8772        let img = &m.media[0];
8773        assert_eq!(img.destination, "img/cat.png");
8774        assert_eq!(img.alt, "a cat");
8775        // The placeholder row named by `rows_span` carries the label a plain
8776        // surface paints and a capable frontend replaces.
8777        let row_text = |r: usize| -> String { m.rows[r].glyphs.iter().map(|g| g.ch).collect() };
8778        assert_eq!(
8779            img.rows_span.end - img.rows_span.start,
8780            1,
8781            "one placeholder row"
8782        );
8783        assert_eq!(row_text(img.rows_span.start), "🖼 a cat");
8784        // The row carries the mark `media_spans` derives the side-table from.
8785        assert!(m.rows[img.rows_span.start].media.is_some());
8786    }
8787
8788    #[test]
8789    fn an_image_without_alt_labels_itself_with_its_filename() {
8790        let m = map("![](photos/beach.jpg)\n");
8791        let row = &m.rows[m.media[0].rows_span.start];
8792        assert_eq!(
8793            row.glyphs.iter().map(|g| g.ch).collect::<String>(),
8794            "🖼 beach.jpg"
8795        );
8796        assert_eq!(m.media[0].alt, "");
8797    }
8798
8799    #[test]
8800    fn an_empty_cells_home_is_read_from_either_shape_of_span() {
8801        // A whole-row span: the cell's pipes are the `col`-th and next.
8802        let row = "|  |  |";
8803        assert_eq!(empty_cell_offset(row, 10, 0), 12);
8804        assert_eq!(empty_cell_offset(row, 10, 1), 15);
8805        // A cell's own span, opening pipe to closing pipe exclusive: the same
8806        // homes, each read from its own span.
8807        assert_eq!(empty_cell_offset("|  ", 10, 0), 12);
8808        assert_eq!(empty_cell_offset("|  ", 13, 1), 15);
8809        // Nothing to stand in: just inside the pipe, never past the span.
8810        assert_eq!(empty_cell_offset("|", 10, 0), 11);
8811        assert_eq!(empty_cell_offset("", 10, 1), 10);
8812    }
8813
8814    #[test]
8815    fn a_hidden_marks_content_end_is_a_caret_home_but_not_a_glyph_stop() {
8816        // `a **bold** b`: the `d` is at 7, the content ends at 8, the closing
8817        // `**` draws nothing, and the space after it is at 10. Two homes at one
8818        // spot on screen: 8 (inside the bold) and 10 (past it).
8819        let m = map("a **bold** b\n");
8820        assert!(
8821            !m.stops.contains(&8),
8822            "8 has no glyph, so it is no glyph stop"
8823        );
8824        assert_eq!(m.mark_ends, vec![8]);
8825        assert!(m.is_stop(8), "but the caret may rest there");
8826        assert_eq!(m.snap_to_stop(8), 8, "and is left there when placed there");
8827        // Left/Right take both homes; the character-pairing walk takes one.
8828        assert_eq!(m.caret_stop_after(7), Some(8));
8829        assert_eq!(m.caret_stop_after(8), Some(10));
8830        assert_eq!(m.caret_stop_before(10), Some(8));
8831        assert_eq!(m.caret_stop_before(8), Some(7));
8832        assert_eq!(m.stop_after(7), Some(10));
8833        assert_eq!(m.stop_before(10), Some(7));
8834        // Drawn where the next glyph is: after the `d`, not on it.
8835        assert_eq!(m.pos_of_offset(8), m.pos_of_offset(10));
8836    }
8837
8838    #[test]
8839    fn every_hidden_inline_mark_gives_its_content_end_a_home() {
8840        // One end per mark, whatever it is spelled with; nested marks closing
8841        // together share the outer's end and the inner's alike.
8842        assert_eq!(
8843            map("*em* `code` [link](u) ~~del~~\n").mark_ends,
8844            vec![3, 10, 17, 27]
8845        );
8846        assert_eq!(map("***both***\n").mark_ends, vec![7]);
8847        // A mark that closes at its row's end coincides with the row's own end
8848        // stop — one offset, in both tables.
8849        let m = map("**bold**\n");
8850        assert_eq!(m.mark_ends, vec![6]);
8851        assert!(m.stops.contains(&6));
8852        // Revealed, the delimiter is glyphs of its own and the end is an
8853        // ordinary glyph stop: nothing to add.
8854        let mut ed = Editor::new_str("a **bold** b\n", Format::Markdown).unwrap();
8855        let src = "a **bold** b\n";
8856        let revealed = build(
8857            &ed.nodes().unwrap(),
8858            src,
8859            Some(80),
8860            false,
8861            &Surface::default(),
8862            Some(Reveal::full(0..src.len())),
8863        );
8864        assert!(revealed.mark_ends.is_empty());
8865        assert!(revealed.stops.contains(&8));
8866    }
8867
8868    #[test]
8869    fn a_marks_content_end_is_a_home_inside_a_table_cell() {
8870        let src = "| A | B |\n| --- | --- |\n| **bold** | other |\n";
8871        let m = map(src);
8872        let end = src.find("bold").unwrap() + 4; // 32, before the closing `**`
8873        assert_eq!(m.mark_ends, vec![end]);
8874        assert_eq!(m.snap_to_stop(end), end);
8875        // Drawn after the `d`, in this cell — where the cell's own end stop is.
8876        assert_eq!(m.pos_of_offset(end), m.pos_of_offset(end + 2));
8877    }
8878
8879    #[test]
8880    fn a_block_media_gives_the_caret_a_home_before_and_after_it() {
8881        // `![x](y)` on its own line: the caret can rest in front of the image
8882        // (its start) and just past it (the row end), and nowhere inside the
8883        // markup — the same coarse mapping a thematic break uses.
8884        let src = "![x](y.png)\n";
8885        let m = map(src);
8886        let img = &m.rows[m.media[0].rows_span.start];
8887        let start = 0; // the image opens the document
8888        let end = "![x](y.png)".len();
8889        // Every placeholder glyph maps to the image start and is a stop there.
8890        assert!(img.glyphs.iter().all(|g| g.src == start && g.stop));
8891        assert_eq!(img.end_src, end, "the row ends past the image");
8892        assert_eq!(m.stops.first(), Some(&start));
8893        assert!(m.stops.contains(&end), "a stop sits after the image");
8894        // Nothing inside the markup is a stop.
8895        assert!(!m.stops.iter().any(|&s| s > start && s < end));
8896    }
8897
8898    #[test]
8899    fn an_inline_image_amid_text_is_not_a_block_media() {
8900        // An image sharing its line with prose isn't block-level: it stays in the
8901        // inline path (rendered as its alt text), and publishes no MediaInfo.
8902        let m = map("see ![a cat](cat.png) here\n");
8903        assert!(m.media.is_empty(), "not a block image");
8904        assert!(
8905            rendered(&m).contains("a cat"),
8906            "alt text still renders inline"
8907        );
8908    }
8909
8910    /// The block images `Doc` publishes for `src`, driven through the real
8911    /// production build (`build_visual` → `build_cached`) with `html_elements`
8912    /// on — the path a `<picture>` actually travels. Not the raw `build` the
8913    /// other tests use: the editor's flat whole-arena snapshot tangles the links
8914    /// of inline-promoted HTML (phantom roots, dangling `parent`s), which only
8915    /// the per-block subtree walk `build_cached` does untangles.
8916    fn doc_media(src: &str) -> Vec<MediaInfo> {
8917        let mut doc = crate::Doc::from_source(src.to_string(), Format::Markdown).unwrap();
8918        doc.build_visual(80);
8919        doc.vmap.media.clone()
8920    }
8921
8922    #[test]
8923    fn a_video_block_is_media_with_its_src_poster_and_kind() {
8924        // The load-bearing assumption of video support: twig has no `video` node
8925        // kind, so `html_elements` promotion must land a `<video>` as a generic
8926        // `element` whose tag name and attributes survive onto `FlatNode` — the
8927        // same treatment `<picture>` gets. If that ever stops holding, this is
8928        // the test that says so.
8929        let m = doc_media("<video src=\"clip.mp4\" poster=\"still.png\" controls>\n</video>\n");
8930        assert_eq!(m.len(), 1, "the video is one block media");
8931        assert_eq!(m[0].kind, MediaKind::Video);
8932        assert_eq!(m[0].destination, "clip.mp4");
8933        assert_eq!(m[0].poster, "still.png");
8934    }
8935
8936    #[test]
8937    fn a_single_line_video_is_a_block_too() {
8938        // The spelling everyone actually writes. It used to parse as a paragraph
8939        // of raw inline HTML — CommonMark opens a block on a complete tag only
8940        // when the line ends there, and its fixed tag list predates `<video>` —
8941        // so the tags never reached core as an element at all. twig 2.5.1 widened
8942        // that list under `html_elements`; this is the test that would catch the
8943        // pin sliding back.
8944        let m = doc_media("<video src=\"clip.mp4\" controls></video>\n");
8945        assert_eq!(m.len(), 1, "single-line <video> is a block");
8946        assert_eq!(m[0].kind, MediaKind::Video);
8947        assert_eq!(m[0].destination, "clip.mp4");
8948    }
8949
8950    #[test]
8951    fn a_single_line_picture_is_a_block_with_its_alternatives() {
8952        // `<picture>` had the identical gap and it went unnoticed because the
8953        // conventional spelling breaks the lines. Same twig fix covers it.
8954        let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\">\
8955                   <img src=\"l.svg\" alt=\"banner\"></picture>\n";
8956        let m = doc_media(src);
8957        assert_eq!(m.len(), 1);
8958        assert_eq!(m[0].kind, MediaKind::Image);
8959        assert_eq!(m[0].destination, "l.svg");
8960        assert_eq!(m[0].resolve(ColorScheme::Dark), "d.svg");
8961    }
8962
8963    #[test]
8964    fn an_audio_block_is_media_with_no_poster() {
8965        let m = doc_media("<audio src=\"take.mp3\" controls>\n</audio>\n");
8966        assert_eq!(m.len(), 1);
8967        assert_eq!(m[0].kind, MediaKind::Audio);
8968        assert_eq!(m[0].destination, "take.mp3");
8969        assert!(m[0].poster.is_empty(), "audio has no poster frame");
8970    }
8971
8972    #[test]
8973    fn a_videos_source_children_are_its_candidates_typed_by_mime() {
8974        // A `<video>` with no `src` of its own — the common shape, since it's how
8975        // you offer more than one codec. The candidates come from `<source src>`
8976        // (not `srcset`, which is `<picture>`'s spelling) and carry their MIME.
8977        let src = "<video controls>\n\
8978                   <source src=\"a.webm\" type=\"video/webm\">\n\
8979                   <source src=\"a.mp4\" type=\"video/mp4\">\n\
8980                   fallback\n\
8981                   </video>\n";
8982        let m = doc_media(src);
8983        assert_eq!(m.len(), 1);
8984        assert!(
8985            m[0].destination.is_empty(),
8986            "no src attribute on the element"
8987        );
8988        assert_eq!(m[0].sources.len(), 2);
8989        assert_eq!(m[0].sources[0].srcset, "a.webm");
8990        assert_eq!(m[0].sources[0].mime, "video/webm");
8991        assert_eq!(m[0].sources[1].srcset, "a.mp4");
8992        // With an empty destination, `resolve` falls through to the first
8993        // candidate rather than handing the frontend nothing to load.
8994        assert_eq!(m[0].resolve(ColorScheme::Light), "a.webm");
8995    }
8996
8997    #[test]
8998    fn a_video_placeholder_row_carries_its_own_sigil_and_mark() {
8999        // The placeholder contract images already hold, now for a video: the row
9000        // renders as a labelled stand-in a plain surface can paint as-is, and
9001        // carries the mark a capable frontend replaces it from.
9002        let src = "<video src=\"clip.mp4\" controls>\n</video>\n";
9003        let mut doc = crate::Doc::from_source(src.to_string(), Format::Markdown).unwrap();
9004        doc.build_visual(80);
9005        let row = &doc.vmap.rows[doc.vmap.media[0].rows_span.start];
9006        let text: String = row.glyphs.iter().map(|g| g.ch).collect();
9007        assert!(
9008            text.starts_with('🎬'),
9009            "video sigil, not the image one: {text:?}"
9010        );
9011        assert!(row.media.is_some(), "the mark rides the placeholder row");
9012    }
9013
9014    #[test]
9015    fn a_picture_block_carries_its_source_alternatives() {
9016        // A `<picture>` with a dark-mode `<source>`: one block image, whose
9017        // fallback destination is the `<img>` and whose `sources` carry the
9018        // `<source>`'s media + srcset for a theme-aware frontend to pick.
9019        let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"dark.svg\"><img src=\"light.svg\" alt=\"banner\"></picture>\n";
9020        let images = doc_media(src);
9021        assert_eq!(images.len(), 1, "the picture is one block image");
9022        let img = &images[0];
9023        assert_eq!(img.destination, "light.svg", "fallback is the <img>");
9024        assert_eq!(img.alt, "banner");
9025        assert_eq!(
9026            img.sources,
9027            vec![MediaSource {
9028                media: "(prefers-color-scheme: dark)".into(),
9029                srcset: "dark.svg".into(),
9030                mime: String::new(),
9031            }],
9032        );
9033    }
9034
9035    #[test]
9036    fn a_picture_inside_a_heading_is_still_a_block_media_with_sources() {
9037        // fig.md's shape: the banner is an `<h1>` wrapping the `<picture>`.
9038        let src = "<h1><picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\"><img src=\"l.svg\" alt=\"fig\"></picture></h1>\n";
9039        let images = doc_media(src);
9040        assert_eq!(images.len(), 1, "heading-wrapped picture is a block image");
9041        assert_eq!(images[0].destination, "l.svg");
9042        assert_eq!(images[0].sources.len(), 1);
9043        assert_eq!(images[0].sources[0].srcset, "d.svg");
9044    }
9045
9046    #[test]
9047    fn a_plain_image_has_no_media_sources() {
9048        // A bare Markdown image carries an empty `sources` — nothing to pick from.
9049        let images = doc_media("![alt](p.png)\n");
9050        assert_eq!(images.len(), 1);
9051        assert!(
9052            images[0].sources.is_empty(),
9053            "no <picture>, no alternatives"
9054        );
9055    }
9056
9057    #[test]
9058    fn resolve_picks_the_source_matching_the_scheme() {
9059        let src = "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"dark.svg\"><img src=\"light.svg\" alt=\"b\"></picture>\n";
9060        let images = doc_media(src);
9061        let img = &images[0];
9062        // Dark theme takes the dark source; light falls through to the <img>.
9063        assert_eq!(img.resolve(ColorScheme::Dark), "dark.svg");
9064        assert_eq!(img.resolve(ColorScheme::Light), "light.svg");
9065    }
9066
9067    #[test]
9068    fn resolve_falls_back_for_a_plain_image_and_unknown_media() {
9069        // A plain image ignores the scheme.
9070        let plain = doc_media("![a](p.png)\n");
9071        assert_eq!(plain[0].resolve(ColorScheme::Dark), "p.png");
9072
9073        // A <source> with an unrecognized media query is skipped; a light source
9074        // is taken under a light theme.
9075        let m = doc_media(
9076            "<picture><source media=\"print\" srcset=\"p.svg\"><source media=\"(prefers-color-scheme: light)\" srcset=\"l.svg\"><img src=\"f.svg\" alt=\"x\"></picture>\n",
9077        );
9078        assert_eq!(m[0].resolve(ColorScheme::Light), "l.svg");
9079        assert_eq!(
9080            m[0].resolve(ColorScheme::Dark),
9081            "f.svg",
9082            "no dark source → <img>"
9083        );
9084    }
9085
9086    #[test]
9087    fn resolve_reads_the_first_srcset_url_ignoring_descriptors() {
9088        // A comma/descriptor srcset resolves to its first URL.
9089        assert_eq!(first_srcset_url("a.png 1x, b.png 2x"), Some("a.png"));
9090        assert_eq!(first_srcset_url("  solo.svg  "), Some("solo.svg"));
9091        assert_eq!(first_srcset_url(""), None);
9092        // An empty (unconditional) media always matches.
9093        assert!(media_matches("", ColorScheme::Light));
9094        assert!(media_matches(
9095            "(prefers-color-scheme:dark)",
9096            ColorScheme::Dark
9097        ));
9098        assert!(!media_matches(
9099            "(prefers-color-scheme: dark)",
9100            ColorScheme::Light
9101        ));
9102    }
9103
9104    #[test]
9105    fn a_block_media_carries_its_list_prefix() {
9106        // An image that is a list item's body opens past the bullet, like every
9107        // other block does.
9108        let m = map("- ![alt](p.png)\n");
9109        let row = &m.rows[m.media[0].rows_span.start];
9110        let text: String = row.glyphs.iter().map(|g| g.ch).collect();
9111        assert!(
9112            text.starts_with("• "),
9113            "the list marker prefixes the image row: {text:?}"
9114        );
9115        assert!(text.contains("🖼 alt"));
9116    }
9117
9118    #[test]
9119    fn the_structural_table_spans_exactly_its_drawn_rows() {
9120        // A frontend drawing its own grid skips `rows_span` and renders from
9121        // `grid`. If the span were short the leftover border rows would be
9122        // painted as text under the real table; if long it would eat a
9123        // neighbouring paragraph. Both are silent, so pin it to the picture.
9124        let m = map(&format!("before\n\n{TABLE}\nafter\n"));
9125        let t = &m.tables[0];
9126        let row_text = |r: usize| -> String { m.rows[r].glyphs.iter().map(|g| g.ch).collect() };
9127        assert!(
9128            row_text(t.rows_span.start).starts_with('┌'),
9129            "opens on the top border"
9130        );
9131        assert!(
9132            row_text(t.rows_span.end - 1).starts_with('└'),
9133            "closes on the bottom border"
9134        );
9135        assert!(
9136            !row_text(t.rows_span.start - 1).contains('┌'),
9137            "the row before the span is not the table's"
9138        );
9139        assert_eq!(
9140            row_text(t.rows_span.end),
9141            "",
9142            "the span ends before the gap row"
9143        );
9144    }
9145
9146    #[test]
9147    fn a_nested_tables_structure_carries_the_block_prefix() {
9148        // The picture puts the quote's gutter on every row of the grid. A
9149        // frontend drawing its own table has to draw that too and start past it,
9150        // so the prefix has to travel with the structure — without it a quoted
9151        // table renders flush at the margin and leaves the quote it's in.
9152        let m = map("> | a | b |\n> |---|---|\n> | c | d |\n");
9153        let t = &m.tables[0];
9154        let prefix: String = t.prefix.iter().map(|g| g.ch).collect();
9155        assert_eq!(prefix, "│ ", "the quote's gutter should ride the structure");
9156        // And it matches what the picture actually drew.
9157        let drawn: String = m.rows[t.rows_span.start]
9158            .glyphs
9159            .iter()
9160            .map(|g| g.ch)
9161            .collect();
9162        assert!(
9163            drawn.starts_with(&prefix),
9164            "picture and structure disagree: {drawn:?}"
9165        );
9166    }
9167
9168    #[test]
9169    fn a_top_level_table_carries_no_prefix() {
9170        assert!(map(TABLE).tables[0].prefix.is_empty());
9171    }
9172
9173    #[test]
9174    fn structural_cells_are_unwrapped_even_when_the_picture_wraps_them() {
9175        // The picture wraps a cell to its column; a frontend laying the grid out
9176        // in pixels needs the text as the document spells it, before that
9177        // decision. Narrow enough that the drawn cell must break.
9178        let src = "| Name |\n|------|\n| alpha beta gamma |\n";
9179        let mut ed = Editor::new_str(src, Format::Markdown).unwrap();
9180        let m = build_t(&ed.nodes().unwrap(), src, Some(12));
9181        let drawn = rendered(&m);
9182        let cell: String = m.tables[0].grid[1].cells[0]
9183            .glyphs
9184            .iter()
9185            .map(|g| g.ch)
9186            .collect();
9187        assert_eq!(
9188            cell, "alpha beta gamma",
9189            "structure must not carry the wrap"
9190        );
9191        assert!(
9192            drawn.lines().count() > 5,
9193            "the picture should have wrapped, else this proves nothing:\n{drawn}"
9194        );
9195    }
9196
9197    // ── display columns ──────────────────────────────────────────────────────
9198
9199    #[test]
9200    fn a_table_column_is_as_wide_as_its_cells_are_drawn() {
9201        // A column sized by counting characters is drawn narrower than the text
9202        // it has to hold — `你好` is two characters in four cells — and the cell
9203        // spills over the border it is supposed to sit inside, taking the whole
9204        // grid out of square with it. Squareness is the property: every row of a
9205        // grid is drawn to the same column, whatever its cells are spelled with.
9206        for src in [
9207            "| A | B |\n|---|---|\n| 你好 | y |\n",
9208            "| A | B |\n|---|---|\n| a👨‍👩‍👧b | y |\n",
9209            "| A | 漢字 |\n|---|---|\n| x | y |\n",
9210        ] {
9211            let m = map(src);
9212            let widths: Vec<usize> = m.rows.iter().map(|r| r.width()).collect();
9213            assert!(
9214                widths.windows(2).all(|w| w[0] == w[1]),
9215                "ragged grid {widths:?} for {src:?}:\n{}",
9216                rendered(&m)
9217            );
9218        }
9219    }
9220
9221    #[test]
9222    fn a_cell_wrapped_narrow_never_breaks_inside_a_character() {
9223        // A column too narrow for its cell hard-breaks the text, and every line
9224        // of it is given an end stop just past its last glyph. Broken into runs
9225        // of four glyphs, the first line of this cell ends between `👨‍👩` and the
9226        // joiner holding `👧` on — so its end stop lands inside a character,
9227        // where a click or Down can reach it and the next Backspace takes the
9228        // cluster apart from the middle.
9229        let src = "| A |\n|---|\n| 👨‍👩‍👧👨‍👩‍👧 |\n";
9230        let mut ed = Editor::new_str(src, Format::Markdown).unwrap();
9231        let m = build_t(&ed.nodes().unwrap(), src, Some(8));
9232        let boundaries: Vec<usize> = src
9233            .grapheme_indices(true)
9234            .map(|(i, _)| i)
9235            .chain(std::iter::once(src.len()))
9236            .collect();
9237        for off in (0..=src.len()).filter(|&o| m.is_stop(o)) {
9238            assert!(
9239                boundaries.contains(&off),
9240                "stop at {off} is inside a character:\n{}",
9241                rendered(&m)
9242            );
9243        }
9244    }
9245
9246    #[test]
9247    fn a_wrapped_cell_keeps_every_line_inside_its_column() {
9248        // The width is a promise in a table, where a glyph past the column lands
9249        // on the border or in the next cell — and it is a promise about cells,
9250        // which is not what a count of glyphs measures.
9251        let src = "| A |\n|---|\n| 你好世界漢字 |\n";
9252        let mut ed = Editor::new_str(src, Format::Markdown).unwrap();
9253        let m = build_t(&ed.nodes().unwrap(), src, Some(14));
9254        for r in &m.rows {
9255            assert_eq!(r.width(), 14, "{:?} is not drawn to the grid", rendered(&m));
9256        }
9257    }
9258
9259    #[test]
9260    fn a_hard_break_falls_between_clusters_and_measures_in_cells() {
9261        let glyphs = |s: &str| {
9262            let mut out = Vec::new();
9263            push_text(&mut out, s, 0, Style::default());
9264            out
9265        };
9266        let piece = |p: &[Glyph]| p.iter().map(|g| g.ch).collect::<String>();
9267
9268        // Six cells of CJK broken at four: two characters, then one — never
9269        // between the two cells of `好`.
9270        let w = glyphs("你好世");
9271        let pieces: Vec<String> = hard_break(&w, 4).iter().map(|p| piece(p)).collect();
9272        assert_eq!(pieces, ["你好", "世"]);
9273
9274        // A character wider than the column has nowhere legal to break, so it
9275        // keeps its cells rather than being cut in half.
9276        let w = glyphs("你好");
9277        let pieces: Vec<String> = hard_break(&w, 1).iter().map(|p| piece(p)).collect();
9278        assert_eq!(pieces, ["你", "好"]);
9279
9280        // An empty word yields no pieces at all — a double space stays a space.
9281        assert!(hard_break(&[], 4).is_empty());
9282    }
9283
9284    #[test]
9285    fn an_empty_list_item_still_gets_a_bulleted_row_with_a_caret_home() {
9286        // Pressing Enter at the end of a list item opens a new, empty item —
9287        // a childless `list_item`. Without a row of its own the new bullet
9288        // wouldn't appear until something was typed into it (the caret would be
9289        // stranded on an offset no row draws). It now renders as one prefixed
9290        // row whose end is a caret stop, so the bullet shows and the caret lands
9291        // just past the marker.
9292        let m = map("- item\n- \n");
9293        assert_eq!(m.num_rows(), 2, "the empty second item needs its own row");
9294        assert_eq!(
9295            m.rows[1].glyphs.iter().map(|g| g.ch).collect::<String>(),
9296            "• ",
9297            "the empty item draws just its bullet",
9298        );
9299        // Its end is the caret home (past the `- ` marker), and it's a real stop.
9300        assert!(
9301            m.is_stop(m.rows[1].end_src),
9302            "the empty item's caret home is not a stop"
9303        );
9304        assert_eq!(
9305            m.pos_of_offset(m.rows[1].end_src),
9306            (1, 2),
9307            "caret sits after '• '"
9308        );
9309    }
9310
9311    #[test]
9312    fn a_notes_row_range_stops_at_the_note_even_when_it_ends_in_a_link() {
9313        // The peek bug: a note whose body ends in a link has its last byte
9314        // inside the hidden destination, so mapping `end - 1` through
9315        // `pos_of_offset` snapped *forward* — past its own row, past the drawn
9316        // gap, and onto the next note's row. The popover then drew both notes.
9317        let src = "A[^1] B[^2].\n\n[^1]: bare text\n\n[^2]: [title](https://example.com/x)\n\n[^3]: last\n";
9318        let m = map(src);
9319        let body = src.find("[title]").unwrap();
9320        let end = src.find("\n\n[^3]").unwrap();
9321
9322        let (first, last) = m.row_range_for(body..end);
9323        assert_eq!(
9324            first, last,
9325            "a one-block note is one row, not a span onto the next"
9326        );
9327
9328        // The old arithmetic, kept here as the thing that must stay wrong: it
9329        // is what this method exists instead of.
9330        assert_ne!(
9331            m.pos_of_offset(end - 1).0,
9332            last,
9333            "the forward snap still leaves the note's row — that is the whole point",
9334        );
9335
9336        // A note ending in *visible* text was never broken, and still isn't:
9337        // both readings agree there, which is why the original test missed it.
9338        let plain = src.find("bare text").unwrap();
9339        let plain_end = src.find("\n\n[^2]").unwrap();
9340        let (pf, pl) = m.row_range_for(plain..plain_end);
9341        assert_eq!(pf, pl);
9342        assert_eq!(m.pos_of_offset(plain_end - 1).0, pl);
9343    }
9344
9345    #[test]
9346    fn a_row_range_covers_every_row_of_a_block_that_spans_several() {
9347        // The range is a span, not a point: a quote of two paragraphs covers its
9348        // gap row and both of its text rows, so a peek draws the whole thing.
9349        let src = "> one\n>\n> two\n\nafter\n";
9350        let m = map(src);
9351        let (first, last) = m.row_range_for(0..src.find("\n\nafter").unwrap());
9352        assert_eq!((first, last), (0, 2));
9353
9354        // And a range with no visible byte at all still covers the row it opened
9355        // on, rather than collapsing to nothing.
9356        let (f, l) = m.row_range_for(0..1);
9357        assert_eq!((f, l), (0, 0));
9358    }
9359
9360    #[test]
9361    fn an_empty_block_quote_still_gets_a_gutter_row_with_a_caret_home() {
9362        // The peer of the empty list item, and the case that made an empty line
9363        // in a quote draw as plain body text: a childless `block_quote` — a bare
9364        // `> `, which is what the toolbar's Quote button leaves on a blank line —
9365        // has no inner block to carry the gutter, so the whole quote used to
9366        // render as *nothing*. It didn't merely lose its bar; the row went away
9367        // and the caret had no home on it.
9368        let m = map("a\n\n> \n\nb\n");
9369        assert_eq!(
9370            m.rows[2].glyphs.iter().map(|g| g.ch).collect::<String>(),
9371            "│ ",
9372            "the empty quote draws just its gutter",
9373        );
9374        assert!(
9375            m.rows[2]
9376                .glyphs
9377                .iter()
9378                .all(|g| g.style.role == Role::QuoteGutter)
9379        );
9380        assert!(
9381            !m.rows[2].decoration,
9382            "it is a line text can go on, not a drawn gap"
9383        );
9384        assert!(
9385            m.is_stop(m.rows[2].end_src),
9386            "the empty quote's caret home is not a stop"
9387        );
9388        assert_eq!(
9389            m.pos_of_offset(m.rows[2].end_src),
9390            (2, 2),
9391            "caret sits after '│ '"
9392        );
9393
9394        // And a document that is *only* an empty quote still renders a row — it
9395        // used to render none at all, leaving the caret nowhere to stand.
9396        let m = map("> \n");
9397        assert_eq!(m.num_rows(), 1);
9398        assert_eq!(
9399            m.rows[0].glyphs.iter().map(|g| g.ch).collect::<String>(),
9400            "│ "
9401        );
9402    }
9403
9404    #[test]
9405    fn a_quotes_own_trailing_marker_lines_stay_inside_the_quote() {
9406        // Enter at the end of `> a` writes `> a\n>\n> \n`. Those last two lines
9407        // hold no block — a quote's `content_span` stops at its last child — so
9408        // the children walk never reaches them, and they used to fall through to
9409        // the document-level trailing pass, which knows no prefix: the gutter
9410        // stopped and the writer's new line drew as plain prose. Fixable only
9411        // since twig 3.2.0, where the quote's *span* covers its own marker lines
9412        // (`0..3` before, `0..8` now) and there is finally a node saying they
9413        // are the quote's.
9414        let m = map("> a\n>\n> \n");
9415        assert_eq!(m.num_rows(), 3, "one row per line the quote spells");
9416        for (i, row) in m.rows.iter().enumerate() {
9417            let text = row.glyphs.iter().map(|g| g.ch).collect::<String>();
9418            assert!(text.starts_with("│ "), "row {i} lost the gutter: {text:?}");
9419            assert!(
9420                !row.decoration,
9421                "row {i} is a line to type on, not a drawn gap"
9422            );
9423            assert!(m.is_stop(row.end_src), "row {i} has no caret home");
9424        }
9425        assert_eq!(
9426            m.rows[0].glyphs.iter().map(|g| g.ch).collect::<String>(),
9427            "│ a"
9428        );
9429        // Distinct offsets, so ↑/↓ between them moves the caret rather than
9430        // landing twice on the same byte.
9431        assert!(m.rows[0].end_src < m.rows[1].end_src);
9432        assert!(m.rows[1].end_src < m.rows[2].end_src);
9433
9434        // A blank line *after* the quote is not the quote's: it is spelled with
9435        // no marker, so it stays an ordinary boundary and the gutter ends.
9436        let m = map("> a\n\nb\n");
9437        assert_eq!(m.num_rows(), 3);
9438        assert_eq!(
9439            m.rows[2].glyphs.iter().map(|g| g.ch).collect::<String>(),
9440            "b"
9441        );
9442        assert!(
9443            !m.rows[1]
9444                .glyphs
9445                .iter()
9446                .any(|g| g.style.role == Role::QuoteGutter)
9447        );
9448
9449        // Nesting is the case this could get wrong, and the depth has to come
9450        // from which quote's span the line falls in rather than from the row
9451        // above it. A trailing `>` under `> > a` matches only the OUTER quote,
9452        // so it wears one gutter; spell it `> >` and it wears two.
9453        let m = map("> > a\n>\n");
9454        assert_eq!(
9455            m.rows[0].glyphs.iter().map(|g| g.ch).collect::<String>(),
9456            "│ │ a"
9457        );
9458        assert_eq!(
9459            m.rows[1].glyphs.iter().map(|g| g.ch).collect::<String>(),
9460            "│ "
9461        );
9462        let m = map("> > a\n> >\n");
9463        assert_eq!(
9464            m.rows[1].glyphs.iter().map(|g| g.ch).collect::<String>(),
9465            "│ │ "
9466        );
9467
9468        // And a marker line BETWEEN two quoted paragraphs is untouched: that is
9469        // the boundary `emit_separators_before` spells, and it stays a drawn gap
9470        // rather than becoming a line to type on.
9471        let m = map("> a\n>\n> b\n");
9472        assert_eq!(m.num_rows(), 3);
9473        assert!(
9474            m.rows[1].decoration,
9475            "the gap between two quoted blocks is still a gap"
9476        );
9477    }
9478
9479    #[test]
9480    fn an_empty_ordered_item_gets_its_number_and_a_caret_home() {
9481        let m = map("1. item\n2. \n");
9482        assert_eq!(m.num_rows(), 2);
9483        assert_eq!(
9484            m.rows[1].glyphs.iter().map(|g| g.ch).collect::<String>(),
9485            "2. "
9486        );
9487        assert!(m.is_stop(m.rows[1].end_src));
9488        assert_eq!(
9489            m.pos_of_offset(m.rows[1].end_src),
9490            (1, 3),
9491            "caret sits after '2. '"
9492        );
9493    }
9494
9495    #[test]
9496    fn an_empty_headings_caret_home_is_past_its_hidden_marker() {
9497        // The toolbar's H1 on a blank line writes `# ` and nothing else. The row
9498        // it renders is empty (the marker is hidden), so its end *is* its only
9499        // caret stop — and it has to be the offset past the `# `, where typing
9500        // continues the heading. Anchored at the block's start instead, the caret
9501        // drew in front of the hashes and the first character typed there landed
9502        // before them (`x# `), which isn't a heading at all.
9503        let m = map("# \n");
9504        assert_eq!(m.num_rows(), 1);
9505        assert!(m.rows[0].glyphs.is_empty(), "the `# ` marker is hidden");
9506        assert_eq!(m.rows[0].end_src, 2, "the caret home is past the marker");
9507        assert!(m.is_stop(2), "the empty heading's caret home is not a stop");
9508    }
9509
9510    #[test]
9511    fn a_headings_rows_carry_its_level_even_with_nothing_typed_in_it() {
9512        // The row-level fact a proportional frontend sizes a whole line by. An
9513        // empty heading has no glyph to read a `Role::Heading` off, so a renderer
9514        // scanning glyphs drew `# ` (and its caret) at body height until the
9515        // first character landed.
9516        let m = map("# \n");
9517        assert_eq!(
9518            m.rows[0].heading,
9519            Some(1),
9520            "the empty heading knows its level"
9521        );
9522
9523        // Every row of one that wraps, not just the first — and nothing else.
9524        let m = map_at(
9525            "## a heading long enough to wrap over two rows\n\nbody\n",
9526            Some(20),
9527        );
9528        let heads: Vec<Option<u8>> = m.rows.iter().map(|r| r.heading).collect();
9529        assert!(
9530            heads.iter().filter(|h| **h == Some(2)).count() >= 2,
9531            "got {heads:?}"
9532        );
9533        assert_eq!(
9534            m.rows.last().and_then(|r| r.heading),
9535            None,
9536            "the paragraph under it is not a heading",
9537        );
9538    }
9539
9540    #[test]
9541    fn an_empty_heading_leaves_the_rows_under_it_at_their_own_offsets() {
9542        // The row's end is also what the *next* row's separator is measured from,
9543        // so an empty heading that under-reported it shifted every offset below —
9544        // and the blank line under the heading then claimed the same offset as the
9545        // heading's own end. `pos_of_offset` resolves such a tie downstream (a
9546        // soft wrap belongs to the row below), so the caret at the end of the
9547        // heading was drawn two rows lower, on the blank line.
9548        // `text\n\n# \n\n`: the heading's content opens at 8, and the two rows
9549        // under it end at 9 and 10 — the blank line and the document's end.
9550        let m = map("text\n\n# \n\n");
9551        let end = m.rows.last().expect("a trailing blank row").end_src;
9552        assert_eq!(end, 10, "the trailing rows must end at their real offsets");
9553        // The heading's caret home is its own row's, not one shared with a row
9554        // below — the tie that drew the caret two rows down.
9555        assert_eq!(m.pos_of_offset(8), (2, 0), "the empty heading's own row");
9556        assert!(
9557            m.rows[3..].iter().all(|r| r.end_src > 8),
9558            "rows below own later offsets"
9559        );
9560    }
9561
9562    // ── block boundaries ─────────────────────────────────────────────────────
9563
9564    /// Every drawn boundary in `src`, in order, as `(above, below)`.
9565    fn boundaries(m: &VisualMap) -> Vec<(BlockClass, BlockClass)> {
9566        m.rows
9567            .iter()
9568            .filter_map(|r| r.boundary)
9569            .map(|b| (b.above, b.below))
9570            .collect()
9571    }
9572
9573    #[test]
9574    fn a_boundary_says_which_blocks_it_divides() {
9575        use BlockClass::*;
9576        let m = map("one\n\ntwo\n\n# Head\n\ntail\n\n> quoted\n\n```\ncode\n```\n\n");
9577        assert_eq!(
9578            boundaries(&m),
9579            vec![
9580                (Paragraph, Paragraph),
9581                (Paragraph, Heading),
9582                (Heading, Paragraph),
9583                (Paragraph, Quote),
9584                (Quote, Code),
9585                // The blank line the document trails off with is a boundary too
9586                // — it closes the last block above the empty paragraph the caret
9587                // rests on. See `emit_trailing_blank_lines`.
9588                (Code, Paragraph),
9589            ],
9590            "each gap names the pair it falls between, in document order"
9591        );
9592    }
9593
9594    #[test]
9595    fn a_closing_fence_at_the_end_of_the_document_opens_no_phantom_row() {
9596        // The code block's last row ends at its last line of code; the closing
9597        // fence under it has no row. Counted from the row, the fence's line read
9598        // as a trailing blank line and opened an empty paragraph whose offset
9599        // was inside the fence — typing on it wrote into the backticks.
9600        let m = map("para\n\n```\ncode\n```\n");
9601        let texts: Vec<String> = m
9602            .rows
9603            .iter()
9604            .map(|r| r.glyphs.iter().map(|g| g.ch).collect())
9605            .collect();
9606        assert_eq!(texts, vec!["para", "", "code"], "a row under the fence");
9607        assert!(
9608            m.rows.last().is_some_and(|r| r.code),
9609            "the last row is the code"
9610        );
9611        // One more newline is the real empty paragraph, past the fence.
9612        let m = map("para\n\n```\ncode\n```\n\n");
9613        let last = m.rows.last().expect("a trailing row");
9614        assert_eq!(last.end_src, 20, "the trailing row stands past the fence");
9615        assert!(!last.decoration, "the trailing row is somewhere to type");
9616        // A setext underline is the same shape: markup under the last row.
9617        let m = map("Head\n====\n");
9618        assert_eq!(
9619            m.rows.len(),
9620            1,
9621            "a row under the underline: {:?}",
9622            m.rows.len()
9623        );
9624    }
9625
9626    /// Each row as whether it is drawn-only and where it ends — the shape of
9627    /// the blank lines around a block, which is what its text can't show.
9628    fn row_shape(m: &VisualMap) -> Vec<(bool, usize)> {
9629        m.rows.iter().map(|r| (r.decoration, r.end_src)).collect()
9630    }
9631
9632    #[test]
9633    fn the_lines_above_the_first_block_draw_as_the_lines_under_the_last_do() {
9634        // No row was drawn above the first block, so an empty paragraph opened
9635        // there drew nothing. As at the document's end, one blank line is only
9636        // the gap, and every line above that gap is somewhere to type.
9637        assert_eq!(row_shape(&map("\nb\n")), [(false, 2)]);
9638        let m = map("\n\nb\n");
9639        assert_eq!(row_shape(&m), [(false, 0), (true, 1), (false, 3)]);
9640        assert_eq!(m.content_start, 0, "the caret can reach the empty line");
9641        assert_eq!(m.pos_of_offset(0), (0, 0));
9642        assert_eq!(map("b\n").content_start, 0);
9643        assert_eq!(
9644            row_shape(&map("\n\n\n# H\n")),
9645            [(false, 0), (false, 1), (true, 2), (false, 6)]
9646        );
9647
9648        // Past frontmatter, the line under it is the frontmatter's.
9649        let fm = "---\nt: x\n---\n";
9650        let m = map(&format!("{fm}\nb\n"));
9651        assert_eq!(row_shape(&m), [(false, 15)]);
9652        assert_eq!(m.content_start, 14);
9653        let m = map(&format!("{fm}\n\nb\n"));
9654        assert_eq!(row_shape(&m), [(false, 13), (true, 14), (false, 16)]);
9655        assert_eq!(m.content_start, 13);
9656
9657        // Preserve flow draws every line, but the frontmatter's.
9658        let preserve = |src: &str| {
9659            let mut ed = Editor::new_str(src, Format::Markdown).unwrap();
9660            build(
9661                &ed.nodes().unwrap(),
9662                src,
9663                Some(80),
9664                true,
9665                &Surface::default(),
9666                None,
9667            )
9668        };
9669        assert_eq!(row_shape(&preserve("\nb\n")), [(false, 0), (false, 2)]);
9670        assert_eq!(row_shape(&preserve(&format!("{fm}\nb\n"))), [(false, 15)]);
9671        assert_eq!(
9672            row_shape(&preserve(&format!("{fm}\n\nb\n"))),
9673            [(false, 14), (false, 16)]
9674        );
9675
9676        // Past a comment, counted from the line after it.
9677        assert_eq!(row_shape(&map("<!-- c -->\n\nb\n")), [(false, 13)]);
9678        assert_eq!(
9679            row_shape(&map("<!-- c -->\n\n\nb\n")),
9680            [(false, 11), (true, 12), (false, 14)]
9681        );
9682    }
9683
9684    #[test]
9685    fn the_lines_under_a_rule_are_counted_from_the_rule() {
9686        // A rule's row ends at the caret's home past the newline under it, and
9687        // the counts of the blank lines below used to start from there too, so
9688        // every gap under a rule came up a line short: the empty paragraph
9689        // Enter opens beneath a rule drew as two gaps and no line, and the
9690        // caret put on it was drawn on the next block. They count from the
9691        // rule itself, as under any other block's last line.
9692        let src = "a\n\n---\n\n\n\nb\n";
9693        let m = map(src);
9694        assert_eq!(
9695            row_shape(&m),
9696            [
9697                (false, 1),
9698                (true, 2),
9699                (false, 7), // the rule, its home past the newline under it
9700                (true, 7),
9701                (false, 8), // the empty paragraph
9702                (true, 9),
9703                (false, 11),
9704            ]
9705        );
9706        assert_eq!(m.pos_of_offset(8), (4, 0), "the caret on the empty line");
9707        // The one blank line under a rule is still the one gap, not two.
9708        assert_eq!(
9709            row_shape(&map("a\n\n---\n\nb\n")),
9710            [(false, 1), (true, 2), (false, 7), (true, 7), (false, 9)]
9711        );
9712
9713        // Closing the document, the line under the gap is somewhere to type.
9714        let m = map("a\n\n---\n\n");
9715        assert_eq!(
9716            row_shape(&m),
9717            [(false, 1), (true, 2), (false, 7), (true, 7), (false, 8)]
9718        );
9719        assert_eq!(m.pos_of_offset(8), (4, 0));
9720        // And a lone newline after the rule is only the rule's own.
9721        assert_eq!(
9722            row_shape(&map("a\n\n---\n")),
9723            [(false, 1), (true, 2), (false, 7)]
9724        );
9725
9726        // A quote's own trailing lines under a rule: each a row, the first too.
9727        let m = map("> ---\n>\n> ");
9728        assert_eq!(row_shape(&m), [(false, 6), (false, 7), (false, 10)]);
9729    }
9730
9731    #[test]
9732    fn the_home_past_a_djot_rule_is_the_line_under_it() {
9733        // djot's span takes in the newline that ends the rule, and the home
9734        // past the rule was measured from that end — one newline further on,
9735        // across the blank line, to the next block's first character. A caret
9736        // left after the rule was drawn on that block.
9737        let m = map_djot("a\n\n* * *\n\nb\n");
9738        assert_eq!(
9739            row_shape(&m),
9740            [(false, 1), (true, 2), (false, 9), (true, 9), (false, 11)]
9741        );
9742        assert_eq!(m.pos_of_offset(9).0, 2, "beside the rule, not on `b`");
9743        let m = map_djot("a\n\n* * *\n\n\n\nb\n");
9744        assert_eq!(m.pos_of_offset(10), (4, 0), "the caret on the empty line");
9745    }
9746
9747    // ── hidden blocks ────────────────────────────────────────────────────────
9748
9749    /// The row texts of `m`, one string per row.
9750    fn row_texts(m: &VisualMap) -> Vec<String> {
9751        m.rows
9752            .iter()
9753            .map(|r| r.glyphs.iter().map(|g| g.ch).collect())
9754            .collect()
9755    }
9756
9757    #[test]
9758    fn a_div_s_closing_tag_is_not_a_blank_row() {
9759        // The `</div>` sits on a line of its own under the div's last child and
9760        // draws nothing. Counting the separator from the child's end read that
9761        // line as a blank line between the div and the block below — a
9762        // navigable empty row the author never opened — and at the end of the
9763        // file, as an empty trailing paragraph.
9764        let m = map("above\n\n<div class=\"center\">\n\nhello\n\n</div>\n\nbelow\n");
9765        assert_eq!(row_texts(&m), ["above", "", "hello", "", "below"]);
9766        assert!(!m.is_stop(36), "the `</div>` line is not a caret home");
9767        assert_eq!(
9768            m.stop_after(34),
9769            Some(44),
9770            "from `hello` the next stop is `below`"
9771        );
9772
9773        let m = map("above\n\n<div class=\"center\">\n\nhello\n\n</div>\n");
9774        assert_eq!(row_texts(&m), ["above", "", "hello"], "no trailing rows");
9775    }
9776
9777    #[test]
9778    fn a_comment_between_two_blocks_is_stepped_over_not_drawn_as_a_gap() {
9779        // `<!-- exec -->` is a top-level block that draws no rows. The blocks
9780        // either side of it meet across the one boundary a paragraph and a code
9781        // block always meet across — not that boundary *plus* one blank row per
9782        // line of the comment, which is what counting the separator from the
9783        // paragraph's end used to spell.
9784        let m = map("para one\n\n<!-- exec -->\n```\ncode\n```\n\nafter\n");
9785        assert_eq!(row_texts(&m), ["para one", "", "code", "", "after"]);
9786        assert_eq!(
9787            boundaries(&m),
9788            vec![
9789                (BlockClass::Paragraph, BlockClass::Code),
9790                (BlockClass::Code, BlockClass::Paragraph),
9791            ],
9792            "the boundary names the drawn blocks either side, not the comment"
9793        );
9794        // The gap stands past the comment, so the caret's row lookup never
9795        // resolves inside it.
9796        assert_eq!(
9797            m.rows[1].end_src, 23,
9798            "the gap row ends at the comment's end"
9799        );
9800    }
9801
9802    #[test]
9803    fn a_comment_opening_the_document_draws_no_leading_gap() {
9804        let m = map("<!-- lead -->\n\npara\n");
9805        assert_eq!(row_texts(&m), ["para"]);
9806        assert_eq!(m.content_start, 0, "the comment is still the first block");
9807    }
9808
9809    #[test]
9810    fn a_comment_closing_the_document_is_not_trailing_blank_lines() {
9811        // Its lines are not blank lines the author opened with Enter, so no
9812        // gap-plus-empty-paragraph is fabricated under the last drawn block.
9813        let m = map("para\n\n<!-- trail -->\n");
9814        assert_eq!(row_texts(&m), ["para"]);
9815        // Enter at the end of the document still opens the empty paragraph the
9816        // caret rests on: the newlines *after* the comment count as they would
9817        // after any block.
9818        let m = map("para\n\n<!-- trail -->\n\n");
9819        assert_eq!(row_texts(&m), ["para", "", ""]);
9820    }
9821
9822    #[test]
9823    fn a_comment_in_a_list_item_leaves_the_bullet_to_what_follows_it() {
9824        // The first *drawn* child wears the item's marker; a hidden first child
9825        // would otherwise take it and leave the text without one.
9826        let m = map("- <!-- note -->\n\n  text\n- two\n");
9827        let texts = row_texts(&m);
9828        assert!(
9829            texts.iter().any(|t| t == "• text"),
9830            "the text wears the bullet: {texts:?}"
9831        );
9832        assert!(
9833            !texts.iter().any(|t| t == "• "),
9834            "no empty bullet row for the comment: {texts:?}"
9835        );
9836    }
9837
9838    #[test]
9839    fn the_cached_build_does_not_spell_the_document_out_as_blank_rows_after_a_comment() {
9840        // The bug as seen: a 200-line document with one comment in it rendered
9841        // ~200 blank rows after the comment, one per source line, because the
9842        // comment's per-block builder handed back a `last_off` of 0. Parity with
9843        // `build` alone would not catch a *shared* wrong answer, so the count is
9844        // pinned outright.
9845        let body = (0..200)
9846            .map(|i| format!("line {i}"))
9847            .collect::<Vec<_>>()
9848            .join("\n\n");
9849        let src = format!("intro\n\n<!-- exec -->\n{body}\n");
9850        let mut ed = Editor::new_str(&src, Format::Markdown).unwrap();
9851        let mut cache = BlockCache::default();
9852        let (plain, cached) = render_both(&mut ed, &src, Some(80), &mut cache);
9853        assert_maps_eq(&plain, &cached, "comment then 200 paragraphs");
9854        // intro, then 200 × (gap, paragraph): 401 rows and not a row more.
9855        assert_eq!(cached.rows.len(), 401);
9856    }
9857
9858    #[test]
9859    fn a_link_reference_definition_is_stepped_over_like_a_comment() {
9860        // `[a]: /a` is a root beside `doc` with no rows of its own. Merged into
9861        // the walk it is a hidden block: the blocks either side meet across one
9862        // boundary, and its line is not a blank row.
9863        let m = map("see [a]\n\n[a]: /a\n\nafter\n");
9864        assert_eq!(row_texts(&m), ["see a", "", "after"]);
9865        assert_eq!(
9866            boundaries(&m),
9867            vec![(BlockClass::Paragraph, BlockClass::Paragraph)]
9868        );
9869    }
9870
9871    #[test]
9872    fn link_reference_definitions_closing_the_document_are_not_trailing_blank_lines() {
9873        // The README shape: prose, then a `[links]` block nobody reads. Its
9874        // lines used to be counted as blank ones, an empty paragraph per
9875        // definition under the last real block.
9876        let m = map("see [a] and [b]\n\n<!-- links -->\n[a]: /a\n[b]: /b \"bee\"\n");
9877        assert_eq!(row_texts(&m), ["see a and b"]);
9878    }
9879
9880    #[test]
9881    fn a_definition_glued_under_a_paragraph_stays_inside_it() {
9882        // `[a]: /a` at the front of a paragraph's lines is stripped from the
9883        // paragraph's text, but the paragraph's span still starts on its line.
9884        // Both blocks start at the same offset; the definition, sorted first,
9885        // is stepped over, and the paragraph draws as it always did — one gap
9886        // above it, none inside.
9887        let m = map("intro\n\n[a]: /a\ntext [a]\n");
9888        assert_eq!(row_texts(&m), ["intro", "", "text a"]);
9889    }
9890
9891    #[test]
9892    fn a_definition_with_no_span_is_left_out_of_the_walk() {
9893        // twig before 3.3.3 reported `0..0` for every link reference
9894        // definition. One of those has nowhere to be merged: sorted first by
9895        // its zero start it would open the document with a phantom block, and
9896        // the walk would step back to offset 0. It is simply not a block. A
9897        // footnote definition is always placed; it has a body to draw.
9898        assert!(!is_placed_definition(&Kind::Reference, &(0..0)));
9899        assert!(is_placed_definition(&Kind::Reference, &(7..14)));
9900        assert!(is_placed_definition(&Kind::Footnote, &(0..0)));
9901        assert!(!is_placed_definition(&Kind::Str, &(7..14)));
9902    }
9903
9904    #[test]
9905    fn the_trailing_gap_closes_the_last_block() {
9906        // Two Enters at the end of a document: a drawn gap, then the navigable
9907        // empty paragraph. Only the gap is labelled, so a frontend that shrinks
9908        // boundaries shrinks the spacer and leaves the row being typed on alone.
9909        let m = map("# Head\n\n\n");
9910        assert_eq!(
9911            boundaries(&m),
9912            vec![(BlockClass::Heading, BlockClass::Paragraph)]
9913        );
9914    }
9915
9916    #[test]
9917    fn only_the_drawn_gap_rows_carry_a_boundary() {
9918        let m = map("one\n\ntwo\n");
9919        for row in &m.rows {
9920            assert_eq!(
9921                row.boundary.is_some(),
9922                row.decoration,
9923                "a boundary is exactly a drawn gap row: {:?}",
9924                row.glyphs.iter().map(|g| g.ch).collect::<String>()
9925            );
9926        }
9927    }
9928
9929    #[test]
9930    fn preserve_flow_labels_no_boundary() {
9931        // Every blank line is a caret home there — somewhere text can go, not a
9932        // gap between blocks — so nothing is drawn-only and nothing is labelled.
9933        // A frontend keying its spacing off `boundary` can't shrink a row the
9934        // author is about to type on.
9935        let m = map_preserve("one\n\ntwo\n\n# Head\n", Some(80));
9936        assert!(boundaries(&m).is_empty());
9937    }
9938
9939    #[test]
9940    fn a_list_draws_no_boundary_between_its_items() {
9941        // Tight or loose, core puts no gap row between two items of one list —
9942        // so an item↔item boundary is a shape no frontend will ever be handed,
9943        // and spacing one is spacing something that isn't there.
9944        for src in ["- one\n- two\n", "- one\n\n- two\n"] {
9945            let m = map(src);
9946            assert!(
9947                boundaries(&m).is_empty(),
9948                "no gap row inside the list of {src:?}"
9949            );
9950        }
9951        // Leaving the list is an ordinary boundary, and the list is named as
9952        // what sits above it.
9953        let m = map("- one\n- two\n\npara\n");
9954        assert_eq!(
9955            boundaries(&m),
9956            vec![(BlockClass::List, BlockClass::Paragraph)]
9957        );
9958    }
9959
9960    #[test]
9961    fn a_nested_boundary_names_the_blocks_inside_the_container() {
9962        // Two paragraphs inside a blockquote are divided by a Paragraph↔Paragraph
9963        // boundary — the quote is the container they're both in, not what the gap
9964        // separates.
9965        let m = map("> one\n>\n> two\n");
9966        assert_eq!(
9967            boundaries(&m),
9968            vec![(BlockClass::Paragraph, BlockClass::Paragraph)]
9969        );
9970    }
9971
9972    #[test]
9973    fn a_directive_container_draws_one_boundary_like_every_other_block() {
9974        // A container's rows stop at its last *child*, so without anchoring
9975        // `last_off` past the closing `:::` the separator logic counted the fence
9976        // line as a blank row of its own and drew the gap twice — one authored
9977        // blank line, two boundaries, and a frontend spacing each of them put
9978        // double margin under every fenced div. The code-block arm anchors past
9979        // its ``` for exactly this reason; compare the two here.
9980        let fenced = map_directives(":::note\nin\n:::\n\ntwo\n");
9981        assert_eq!(
9982            boundaries(&fenced),
9983            vec![(BlockClass::Directive, BlockClass::Paragraph)],
9984            "one authored gap, one boundary row"
9985        );
9986        let code = map("```\nc\n```\n\ntwo\n");
9987        assert_eq!(
9988            boundaries(&code).len(),
9989            boundaries(&fenced).len(),
9990            "a fenced div spaces like a fenced code block"
9991        );
9992        // Nesting closes several fences at once; still one gap.
9993        let nested = map_directives(":::a\n:::b\nin\n:::\n:::\n\ntwo\n");
9994        assert_eq!(
9995            boundaries(&nested),
9996            vec![(BlockClass::Directive, BlockClass::Paragraph)]
9997        );
9998    }
9999
10000    #[test]
10001    fn a_block_media_names_itself_in_the_boundaries_either_side() {
10002        use BlockClass::*;
10003        // A block image is never a node of its own — `media_only` promotes the
10004        // *paragraph* wrapping it — so classifying the node the walk stands on
10005        // called the picture `Paragraph` and left `BlockClass::Media` unreachable:
10006        // a frontend could not give a photo more air than a line of prose.
10007        // `label_media_boundaries` reads it back off the finished rows instead.
10008        let m = map("one\n\n![alt](p.png)\n\ntwo\n");
10009        assert_eq!(boundaries(&m), vec![(Paragraph, Media), (Media, Paragraph)]);
10010        // At the edges of the document too: the leading gap has no boundary of
10011        // its own, and the trailing one is `emit_trailing_blank_lines`'.
10012        let edges = map("![a](p.png)\n\nmid\n\n![b](q.png)\n");
10013        assert_eq!(
10014            boundaries(&edges),
10015            vec![(Media, Paragraph), (Paragraph, Media)]
10016        );
10017        // One gap spelled with several rows — the row closing the block above and
10018        // the row opening the one below, with the author's spare blank line
10019        // navigable between them — carries the same pair on every drawn row.
10020        let roomy = map("one\n\n\n\n![alt](p.png)\n");
10021        assert_eq!(
10022            boundaries(&roomy),
10023            vec![(Paragraph, Media), (Paragraph, Media)]
10024        );
10025    }
10026
10027    #[test]
10028    fn a_block_video_is_media_at_its_boundaries_not_a_directive_panel() {
10029        // Worse than the image case before `label_media_boundaries`: a `<video>`
10030        // arrives as twig's generic `container`, which classifies `Directive` —
10031        // the one class a frontend reads as "draw a tinted panel here". A movie
10032        // got the chrome of a fenced div.
10033        let mut doc = crate::Doc::from_source(
10034            "one\n\n<video src=\"v.mp4\"></video>\n\ntwo\n".to_string(),
10035            Format::Markdown,
10036        )
10037        .unwrap();
10038        doc.build_visual(80);
10039        assert_eq!(
10040            boundaries(&doc.vmap),
10041            vec![
10042                (BlockClass::Paragraph, BlockClass::Media),
10043                (BlockClass::Media, BlockClass::Paragraph),
10044            ]
10045        );
10046    }
10047
10048    #[test]
10049    fn the_incremental_walk_labels_boundaries_like_the_full_one() {
10050        // `assert_maps_eq` compares boundaries too, so this pins the two doors
10051        // into `BlockClass::from_node_kind` — a `FlatNode`'s kind on the full
10052        // build, a query match's on the cached one — against a document with one
10053        // of every boundary in it.
10054        let src = "one\n\n# Head\n\ntwo\n\n- a\n- b\n\n> q\n\n```\nc\n```\n\npara\n";
10055        let mut ed = Editor::new_str(src, Format::Markdown).unwrap();
10056        let mut cache = BlockCache::default();
10057        let (full, cached) = render_both(&mut ed, src, Some(80), &mut cache);
10058        assert_maps_eq(&full, &cached, "boundary labelling");
10059        assert!(
10060            !boundaries(&full).is_empty(),
10061            "the fixture has boundaries to compare"
10062        );
10063    }
10064
10065    #[test]
10066    fn every_caret_stop_opens_a_cluster_of_its_row() {
10067        // The two ways of finding a cluster have to agree. `push_text` marks the
10068        // stops by segmenting one run of text; the column mapping segments the
10069        // whole row, decoration and all. A stop that came out as the *middle* of
10070        // some row-level cluster would be a caret with no column of its own —
10071        // drawn at the column of whatever swallowed it.
10072        let src = "# 標題\n\na **bold** e\u{0301}mo👨‍👩‍👧ji `x` 你好\n\n\
10073                   - 項目 one\n- e\u{0301}dge\n\n> 引用 text\n\n\
10074                   | A | 值 |\n|---|---|\n| 你好 | 👩‍🚀 |\n";
10075        let m = map(src);
10076        for (r, row) in m.rows.iter().enumerate() {
10077            let openers: Vec<usize> = clusters(&row.glyphs).iter().map(|c| c.glyph).collect();
10078            for (i, g) in row.glyphs.iter().enumerate() {
10079                assert!(
10080                    !g.stop || openers.contains(&i),
10081                    "row {r}: the stop at glyph {i} ({:?}) is inside a cluster, \
10082                     so it is drawn at another glyph's column",
10083                    g.ch
10084                );
10085            }
10086        }
10087    }
10088
10089    // ── the presentation vocabulary ─────────────────────────────────────────
10090
10091    /// A block's own attributes, in the three formats that spell one on the
10092    /// block itself: HTML's tag, djot's `{…}` line, and — the odd one — a
10093    /// Markdown `<div>` around it, which is where twig has to put a Markdown
10094    /// block's attributes because the format has nowhere else.
10095    #[test]
10096    fn a_block_carries_its_alignment_on_every_row_it_draws() {
10097        // HTML, on the paragraph. `lead` is somebody else's class and is
10098        // neither read nor in the way.
10099        let html = map_leaf("<p class=\"lead center\">hi</p>\n", Format::Html);
10100        assert_eq!(line_facts(&html), vec![(Some(Align::Center), None)]);
10101
10102        // djot's attribute line, on the block.
10103        let dj = map_leaf("{.right}\nhi\n", Format::Djot);
10104        assert_eq!(line_facts(&dj), vec![(Some(Align::Right), None)]);
10105
10106        // A heading carries it too, and on every row a wrapped one draws.
10107        let h = map_leaf("{.center}\n# a heading\n", Format::Djot);
10108        assert_eq!(line_facts(&h), vec![(Some(Align::Center), None)]);
10109        assert_eq!(h.rows[0].heading, Some(1));
10110
10111        // Both keys at once, and the line spacing is read the same way.
10112        let both = map_leaf("{.justify data-line-height=\"1.5\"}\nhi\n", Format::Djot);
10113        assert_eq!(
10114            line_facts(&both),
10115            vec![(
10116                Some(Align::Justify),
10117                Some(LineHeight::Step(LineSpacing::OneHalf))
10118            )]
10119        );
10120
10121        // An unknown token is somebody else's and the block draws at the
10122        // theme's alignment; a ratio outside the menu's three is the author's
10123        // own and draws at exactly what they wrote.
10124        let other = map_leaf("{.lead data-line-height=\"1.3\"}\nhi\n", Format::Djot);
10125        assert_eq!(line_facts(&other), vec![(None, LineHeight::ratio(1.3))]);
10126
10127        // A value the grammar does not cover is neither: carried by the
10128        // document, drawn at the theme's spacing, and read as nothing at all.
10129        let em = map_leaf("{data-line-height=\"1.3em\"}\nhi\n", Format::Djot);
10130        assert_eq!(line_facts(&em), vec![(None, None)]);
10131    }
10132
10133    /// `<div class="center">` around three paragraphs centres all three, which
10134    /// is what the author of that HTML meant — and around one is the sole-child
10135    /// shape twig's `set_block_attrs` writes in Markdown.
10136    #[test]
10137    fn a_div_lends_its_alignment_to_every_block_inside_it() {
10138        let m = map_leaf(
10139            "<div class=\"center\" data-line-height=\"2\">\n\none\n\ntwo\n\n</div>\n",
10140            Format::Markdown,
10141        );
10142        assert_eq!(
10143            line_facts(&m),
10144            vec![
10145                (
10146                    Some(Align::Center),
10147                    Some(LineHeight::Step(LineSpacing::Double))
10148                ),
10149                (
10150                    Some(Align::Center),
10151                    Some(LineHeight::Step(LineSpacing::Double))
10152                ),
10153            ]
10154        );
10155
10156        // The nearer node wins, and the block after the div is untouched — the
10157        // context is restored, not left running.
10158        let nested = map_leaf(
10159            "<div class=\"center\">\n\n<div class=\"right\">\n\ninner\n\n</div>\n\nouter\n\n</div>\n\nafter\n",
10160            Format::Markdown,
10161        );
10162        assert_eq!(
10163            line_facts(&nested),
10164            vec![
10165                (Some(Align::Right), None),
10166                (Some(Align::Center), None),
10167                (None, None),
10168            ]
10169        );
10170    }
10171
10172    /// Size, face and colour are the run's, and the block's when the whole
10173    /// block is meant — read at both levels with the nearer winning.
10174    #[test]
10175    fn a_span_s_size_beats_its_block_s_and_its_face_falls_through() {
10176        // `<div data-font>` over `<p data-size>` over `<span data-size>`: the
10177        // span wins on size, the block is still what says the face.
10178        // The span is not first on its line: a `<span …>` opening one is an
10179        // HTML *block* to CommonMark, which is a fact about Markdown and not
10180        // about this.
10181        let m = map_leaf(
10182            "<div data-font=\"serif\">\n\nc <span data-size=\"small\">a</span> b\n\n</div>\n",
10183            Format::Markdown,
10184        );
10185        let a = style_of(&m, 'a');
10186        assert_eq!(a.size, Some(FontSize::Step(SizeStep::Small)));
10187        assert_eq!(a.font, Some(FaceRef::Generic(FontFamily::Serif)));
10188        // The text outside the span keeps the div's face and no size at all.
10189        let b = style_of(&m, 'b');
10190        assert_eq!(b.size, None);
10191        assert_eq!(b.font, Some(FaceRef::Generic(FontFamily::Serif)));
10192
10193        // djot spells the same span anonymously and it reads identically.
10194        let dj = map_leaf(
10195            "{data-size=\"large\"}\nx [y]{data-size=\"xx-large\" data-color=\"blue\"} z\n",
10196            Format::Djot,
10197        );
10198        assert_eq!(
10199            style_of(&dj, 'x').size,
10200            Some(FontSize::Step(SizeStep::Large))
10201        );
10202        assert_eq!(
10203            style_of(&dj, 'y').size,
10204            Some(FontSize::Step(SizeStep::XxLarge))
10205        );
10206        assert_eq!(
10207            style_of(&dj, 'y').color,
10208            Some(TextColor::Named(MarkColor::Blue))
10209        );
10210        // The block's size is still the block's outside the span.
10211        assert_eq!(
10212            style_of(&dj, 'z').size,
10213            Some(FontSize::Step(SizeStep::Large))
10214        );
10215        assert_eq!(style_of(&dj, 'z').color, None);
10216    }
10217
10218    /// The exact half of the vocabulary reaches a glyph and a row by the same
10219    /// doors the names do — the fold has one rule, not one per form. A named
10220    /// family is the one that cannot ride the glyph as itself: the walker
10221    /// interns it and the glyph carries the id.
10222    #[test]
10223    fn an_exact_size_face_and_colour_reach_the_glyph_and_the_row() {
10224        let m = map_leaf(
10225            "<div data-line-height=\"1.3\">\n\nc <span data-size=\"14pt\" \
10226             data-color=\"#c03030\" data-font=\"Garamond\">a</span> b\n\n</div>\n",
10227            Format::Markdown,
10228        );
10229        let a = style_of(&m, 'a');
10230        assert_eq!(a.size, FontSize::points(14.0));
10231        assert_eq!(
10232            a.color,
10233            Some(TextColor::Rgb {
10234                r: 0xc0,
10235                g: 0x30,
10236                b: 0x30
10237            })
10238        );
10239        assert_eq!(a.font, Some(FaceRef::Named(FaceId::of("Garamond"))));
10240        assert_eq!(m.face_name(FaceId::of("Garamond")), Some("Garamond"));
10241        // The div's ratio is the row's, on every row the block draws.
10242        assert_eq!(line_facts(&m), vec![(None, LineHeight::ratio(1.3))]);
10243        // And the text outside the span has none of the span's three.
10244        let b = style_of(&m, 'b');
10245        assert_eq!((b.size, b.font, b.color), (None, None, None));
10246
10247        // One name, one entry, however many spans wear it — the table is what
10248        // keeps a `Style` `Copy` and it should not grow per run.
10249        let twice = map_leaf(
10250            "x <span data-font=\"Garamond\">a</span> y <span data-font=\"Garamond\">b</span>\n",
10251            Format::Markdown,
10252        );
10253        assert_eq!(twice.faces().len(), 1);
10254        assert_eq!(
10255            style_of(&twice, 'a').font,
10256            style_of(&twice, 'b').font,
10257            "one family, one id"
10258        );
10259
10260        // A generic needs no entry at all: it names itself.
10261        let generic = map_leaf(
10262            "<div data-font=\"serif\">\n\nhi\n\n</div>\n",
10263            Format::Markdown,
10264        );
10265        assert_eq!(
10266            style_of(&generic, 'h').font,
10267            Some(FaceRef::Generic(FontFamily::Serif))
10268        );
10269        assert!(generic.faces().is_empty());
10270    }
10271
10272    /// The one key two nodes share. `data-color` on a `mark` is the highlight's
10273    /// *background* and reaches a glyph through [`Role::Mark`]; the same key on
10274    /// an attributed span is the text's foreground. Same vocabulary, same enum,
10275    /// no collision — and a mark inside a coloured span wears both.
10276    #[test]
10277    fn a_mark_keeps_its_highlight_colour_and_a_span_colours_the_text() {
10278        let m = map_leaf("a ==\u{1f534} red== b\n", Format::Markdown);
10279        let r = style_of(&m, 'r');
10280        assert_eq!(r.role, Role::Mark(Some(MarkColor::Red)));
10281        assert_eq!(r.color, None, "a highlight is not a text colour");
10282
10283        let both = map_leaf(
10284            "<span data-color=\"blue\">a ==\u{1f534} red== b</span>\n",
10285            Format::Markdown,
10286        );
10287        let r = style_of(&both, 'r');
10288        assert_eq!(r.role, Role::Mark(Some(MarkColor::Red)), "the highlight");
10289        assert_eq!(
10290            r.color,
10291            Some(TextColor::Named(MarkColor::Blue)),
10292            "the letters"
10293        );
10294    }
10295
10296    /// A page break is the `::page-break` leaf directive, and djot spells the
10297    /// same document as an empty `::: page-break` fence whose name comes back
10298    /// as a class, HTML as a `<page-break>` element and AsciiDoc as `<<<`. All
10299    /// four draw the placeholder row every leaf directive gets and carry the
10300    /// same [`DirectiveMark`], because a frontend that opens a page at one
10301    /// must not be able to tell which format the file is in.
10302    #[test]
10303    fn a_page_break_reads_the_same_in_every_format() {
10304        for (fmt, src) in [
10305            (Format::Markdown, "a\n\n::page-break\n\nb\n"),
10306            (Format::Djot, "a\n\n::: page-break\n:::\n\nb\n"),
10307            (
10308                Format::Html,
10309                "<p>a</p>\n\n<page-break></page-break>\n\n<p>b</p>\n",
10310            ),
10311            (Format::Asciidoc, "a\n\n<<<\n\nb\n"),
10312        ] {
10313            let m = map_leaf(src, fmt);
10314            let marks: Vec<&DirectiveMark> = m
10315                .rows
10316                .iter()
10317                .filter_map(|r| r.leaf_directive.as_ref())
10318                .collect();
10319            assert_eq!(marks.len(), 1, "{fmt:?} draws one placeholder");
10320            assert_eq!(marks[0].name, "page-break", "{fmt:?}");
10321            assert!(marks[0].attrs.is_empty(), "{fmt:?}: {:?}", marks[0].attrs);
10322            assert!(
10323                m.rows
10324                    .iter()
10325                    .any(|r| r.glyphs.iter().map(|g| g.ch).collect::<String>()
10326                        == "\u{29c9} page-break"),
10327                "{fmt:?} draws the label, got {:?}",
10328                m.rows
10329                    .iter()
10330                    .map(|r| r.glyphs.iter().map(|g| g.ch).collect::<String>())
10331                    .collect::<Vec<_>>()
10332            );
10333        }
10334
10335        // A Markdown `:::note` with nothing in it is *not* this: its name is
10336        // its own, and nothing about it says "a block with no body" the way
10337        // djot's spelling of a leaf directive does.
10338        let empty_fence = map_leaf("::: note\n:::\n", Format::Markdown);
10339        assert!(
10340            empty_fence.rows.iter().all(|r| r.leaf_directive.is_none()),
10341            "a named empty fence keeps the reading it has"
10342        );
10343    }
10344
10345    /// A djot fence carrying more than its name keeps the rest as an attribute
10346    /// rather than folding it into the name: the *first* class token is the
10347    /// name, because that is where `insert_directive` puts it.
10348    #[test]
10349    fn a_djot_fence_s_first_class_is_the_directive_s_name_and_the_rest_is_attributes() {
10350        let m = map_leaf("{.page-break .wide}\n:::\n:::\n", Format::Djot);
10351        let mark = m
10352            .rows
10353            .iter()
10354            .find_map(|r| r.leaf_directive.as_ref())
10355            .expect("a placeholder");
10356        assert_eq!(mark.name, "page-break");
10357        assert_eq!(
10358            mark.attrs,
10359            vec![("class".to_string(), Some("wide".to_string()))]
10360        );
10361    }
10362
10363    // ── math ─────────────────────────────────────────────────────────────────
10364
10365    /// [`map_leaf`] on a chosen surface and reveal — the whole of what a math
10366    /// rendering turns on.
10367    fn map_math(src: &str, format: Format, surface: &Surface, reveal: Option<Reveal>) -> VisualMap {
10368        let mut ed =
10369            Editor::new_ext(src.as_bytes(), format, crate::doc::parse_extensions()).unwrap();
10370        build(&ed.nodes().unwrap(), src, Some(80), false, surface, reveal)
10371    }
10372
10373    fn pictures() -> Surface {
10374        Surface {
10375            inline_pictures: true,
10376            ..Default::default()
10377        }
10378    }
10379
10380    #[test]
10381    fn markdown_reads_math_and_a_dollar_before_whitespace_stays_prose() {
10382        // The `math` extension is on for every leaf document; twig's own rule
10383        // keeps a price out of it.
10384        let m = map_leaf("Say $E = mc^2$ for $5 and $6.\n", Format::Markdown);
10385        assert_eq!(row_texts(&m), vec!["Say E = mc^2 for $5 and $6."]);
10386        let e = m.rows[0].glyphs.iter().find(|g| g.ch == 'E').unwrap();
10387        assert_eq!(
10388            e.style.role,
10389            Role::Code,
10390            "the formula's TeX, in the code style"
10391        );
10392        assert_eq!(e.src, 5, "at its own byte, past the `$`");
10393        let five = m.rows[0].glyphs.iter().find(|g| g.ch == '5').unwrap();
10394        assert_eq!(five.style.role, Role::Body);
10395        assert!(m.math.is_empty(), "no picture stands in on a plain surface");
10396    }
10397
10398    #[test]
10399    fn a_plain_surface_draws_inline_math_as_code_with_the_delimiters_hidden() {
10400        // The verbatim treatment, in both languages — what `inline_math` always
10401        // rendered as — with the caret's home at the content's end.
10402        for (src, fmt) in [
10403            ("a $x+y$ b\n", Format::Markdown),
10404            ("a $`x+y` b\n", Format::Djot),
10405        ] {
10406            let m = map_leaf(src, fmt);
10407            assert_eq!(row_texts(&m), vec!["a x+y b"], "{src:?}");
10408            let x = m.rows[0].glyphs.iter().find(|g| g.ch == 'x').unwrap();
10409            assert_eq!(x.style.role, Role::Code);
10410            assert!(!m.mark_ends.is_empty(), "the content end is a caret home");
10411        }
10412    }
10413
10414    #[test]
10415    fn display_math_in_a_line_of_prose_puts_its_text_at_the_text_s_own_offset() {
10416        // `display_math` had no arm and fell to the default one, which pushed
10417        // the text at the *node's* start: three bytes short in djot, past `$$`
10418        // and the backtick.
10419        let m = map_leaf("Before $$`x`$$ after\n", Format::Djot);
10420        let x = m.rows[0].glyphs.iter().find(|g| g.ch == 'x').unwrap();
10421        assert_eq!(x.src, "Before $$`".len());
10422        assert_eq!(x.style.role, Role::Code);
10423        let m = map_leaf("Before $$x$$ after\n", Format::Markdown);
10424        let x = m.rows[0].glyphs.iter().find(|g| g.ch == 'x').unwrap();
10425        assert_eq!(x.src, "Before $$".len());
10426    }
10427
10428    #[test]
10429    fn a_surface_that_paints_in_a_line_gets_one_atom_per_inline_formula() {
10430        let src = "Say $E = mc^2$ and $a$.\n";
10431        let m = map_math(src, Format::Markdown, &pictures(), None);
10432        assert_eq!(row_texts(&m), vec!["Say ∑ and ∑."]);
10433        assert_eq!(m.math.len(), 2);
10434        let first = &m.math[0];
10435        assert_eq!(first.tex, "E = mc^2");
10436        assert!(!first.display);
10437        assert_eq!(first.row, 0);
10438        assert_eq!(first.rows_span, 0..1);
10439        assert_eq!(first.glyph, Some(4));
10440        assert_eq!(first.src, 4, "the formula's start, where a click lands");
10441        let atom = &m.rows[0].glyphs[4];
10442        assert_eq!(atom.ch, MATH_ATOM);
10443        assert_eq!(atom.style.role, Role::Math);
10444        assert!(atom.stop);
10445        assert_eq!(atom.src, 4);
10446        // The caret has a stop on the atom and the next glyph's past it, and
10447        // nothing inside the markup.
10448        assert_eq!(m.stop_after(4), Some("Say $E = mc^2$".len()));
10449        assert!(m.mark_ends.is_empty(), "no home inside the hidden markup");
10450        // The row carries the marks the side-table is derived from.
10451        assert_eq!(m.rows[0].math.len(), 2);
10452        assert_eq!(m.rows[0].math[1].glyph, Some(m.math[1].glyph.unwrap()));
10453        assert_eq!(m.math[1].tex, "a");
10454    }
10455
10456    #[test]
10457    fn a_display_formula_written_inline_is_an_atom_in_display_style() {
10458        let m = map_math("Before $$x$$ after\n", Format::Markdown, &pictures(), None);
10459        assert_eq!(row_texts(&m), vec!["Before ∑ after"]);
10460        assert_eq!(m.math.len(), 1);
10461        assert!(m.math[0].display);
10462        assert_eq!(m.math[0].tex, "x");
10463    }
10464
10465    #[test]
10466    fn an_atom_follows_its_glyph_across_a_wrap() {
10467        // Fifteen words, then a formula that wraps onto the second row: the
10468        // mark is drained onto the row the glyph landed on, at its index there.
10469        let src = format!("{}$x$ end\n", "word ".repeat(15));
10470        let mut ed = Editor::new_ext(
10471            src.as_bytes(),
10472            Format::Markdown,
10473            crate::doc::parse_extensions(),
10474        )
10475        .unwrap();
10476        let m = build(
10477            &ed.nodes().unwrap(),
10478            &src,
10479            Some(40),
10480            false,
10481            &pictures(),
10482            None,
10483        );
10484        assert!(m.rows.len() >= 2);
10485        assert_eq!(m.math.len(), 1);
10486        let info = &m.math[0];
10487        let g = &m.rows[info.row].glyphs[info.glyph.unwrap()];
10488        assert_eq!(g.ch, MATH_ATOM);
10489        assert_eq!(g.src, src.find("$x$").unwrap());
10490        assert!(m.rows[..info.row].iter().all(|r| r.math.is_empty()));
10491    }
10492
10493    #[test]
10494    fn an_atom_in_a_table_cell_rides_the_cell_s_row() {
10495        let m = map_math(
10496            "| a | b |\n|---|---|\n| $x$ | c |\n",
10497            Format::Markdown,
10498            &pictures(),
10499            None,
10500        );
10501        assert_eq!(m.math.len(), 1);
10502        let info = &m.math[0];
10503        assert_eq!(m.rows[info.row].glyphs[info.glyph.unwrap()].ch, MATH_ATOM);
10504    }
10505
10506    #[test]
10507    fn a_display_formula_on_its_own_lines_is_a_block_placeholder_on_every_surface() {
10508        for (src, fmt) in [
10509            (
10510                "intro\n\n$$\n\\int_0^1 x\\,dx\n$$\n\nend\n",
10511                Format::Markdown,
10512            ),
10513            ("intro\n\n$$`\\int_0^1 x\\,dx`\n\nend\n", Format::Djot),
10514        ] {
10515            for surface in [Surface::default(), pictures()] {
10516                let m = map_math(src, fmt, &surface, None);
10517                assert_eq!(
10518                    row_texts(&m),
10519                    vec!["intro", "", "∑ \\int_0^1 x\\,dx", "", "end"],
10520                    "{src:?}"
10521                );
10522                assert_eq!(m.math.len(), 1);
10523                let info = &m.math[0];
10524                assert!(info.display);
10525                assert_eq!(info.glyph, None, "a block, not an atom");
10526                assert_eq!(info.rows_span, 2..3);
10527                assert_eq!(info.tex.trim(), "\\int_0^1 x\\,dx");
10528                let start = src.find("$$").unwrap();
10529                let end = start + src[start..].find("\n\nend").unwrap();
10530                assert_eq!(info.src, start);
10531                // Every label glyph at the formula's start, a stop there and
10532                // one past the block, nothing inside — a picture's two homes.
10533                let row = &m.rows[2];
10534                assert!(
10535                    row.glyphs
10536                        .iter()
10537                        .all(|g| g.src == start && g.style.role == Role::Math)
10538                );
10539                assert_eq!(row.end_src, end);
10540                assert_eq!(m.stop_after(start), Some(end));
10541                assert_eq!(m.stop_before(end), Some(start));
10542                // The gaps either side are labelled as a formula's.
10543                assert_eq!(m.rows[1].boundary.map(|b| b.below), Some(BlockClass::Math));
10544                assert_eq!(m.rows[3].boundary.map(|b| b.above), Some(BlockClass::Math));
10545            }
10546        }
10547    }
10548
10549    #[test]
10550    fn a_display_block_reserves_the_rows_the_frontend_measured() {
10551        let src = "$$\nx\n$$\n\nend\n";
10552        let surface = Surface {
10553            math_rows: HashMap::from([("\nx\n".to_string(), 4)]),
10554            ..Default::default()
10555        };
10556        let m = map_math(src, Format::Markdown, &surface, None);
10557        assert_eq!(m.math[0].rows_span, 0..4);
10558        assert_eq!(row_texts(&m)[..5], ["∑ x", "", "", "", ""]);
10559        // The fillers are decoration: drawn, no caret, anchored past the block.
10560        for r in &m.rows[1..4] {
10561            assert!(r.decoration);
10562            assert_eq!(r.end_src, 7);
10563        }
10564        assert_eq!(m.stop_after(0), Some(7));
10565        // A height keyed by TeX that does not match reserves nothing.
10566        let surface = Surface {
10567            math_rows: HashMap::from([("x".to_string(), 4)]),
10568            ..Default::default()
10569        };
10570        let m = map_math(src, Format::Markdown, &surface, None);
10571        assert_eq!(m.math[0].rows_span, 0..1);
10572    }
10573
10574    #[test]
10575    fn a_paragraph_with_prose_beside_a_display_formula_is_not_a_block() {
10576        let m = map_math("see\n$$\nx\n$$\n", Format::Markdown, &pictures(), None);
10577        assert!(
10578            m.math.iter().all(|i| i.glyph.is_some()),
10579            "an atom, not a placeholder"
10580        );
10581        let m = map_math(
10582            "$$\nx\n$$\n$$\ny\n$$\n",
10583            Format::Markdown,
10584            &pictures(),
10585            None,
10586        );
10587        assert_eq!(m.math.len(), 2);
10588        assert!(
10589            m.math.iter().all(|i| i.glyph.is_some()),
10590            "two formulas is prose with math in it"
10591        );
10592    }
10593
10594    #[test]
10595    fn a_formula_on_the_reveal_line_is_its_tex_in_every_mode() {
10596        let src = "Say $E = mc^2$ here.\n";
10597        // The hidden modes' reveal: only the formula shows its markup.
10598        let m = map_math(
10599            src,
10600            Format::Markdown,
10601            &pictures(),
10602            Some(Reveal::math(0..src.len() - 1)),
10603        );
10604        assert_eq!(row_texts(&m), vec!["Say $E = mc^2$ here."]);
10605        assert!(
10606            m.math.is_empty(),
10607            "nothing stands in for a revealed formula"
10608        );
10609        let dollar = m.rows[0].glyphs.iter().find(|g| g.ch == '$').unwrap();
10610        assert_eq!(dollar.style.role, Role::Delimiter);
10611        let e = m.rows[0].glyphs.iter().find(|g| g.ch == 'E').unwrap();
10612        assert_eq!(e.style.role, Role::Code);
10613        // Full's reveal draws the same, and an emphasis beside it reveals too
10614        // where the hidden modes' does not.
10615        let src = "*a* $x$\n";
10616        let hidden = map_math(
10617            src,
10618            Format::Markdown,
10619            &pictures(),
10620            Some(Reveal::math(0..src.len() - 1)),
10621        );
10622        assert_eq!(row_texts(&hidden), vec!["a $x$"]);
10623        let full = map_math(
10624            src,
10625            Format::Markdown,
10626            &pictures(),
10627            Some(Reveal::full(0..src.len() - 1)),
10628        );
10629        assert_eq!(row_texts(&full), vec!["*a* $x$"]);
10630        // A reveal line that does not meet the formula leaves the atom.
10631        let other = map_math(
10632            "$x$\n\ntext\n",
10633            Format::Markdown,
10634            &pictures(),
10635            Some(Reveal::math(5..9)),
10636        );
10637        assert_eq!(row_texts(&other)[0], "∑");
10638    }
10639
10640    #[test]
10641    fn a_display_block_on_the_reveal_line_is_its_source_lines_in_the_code_style() {
10642        let src = "intro\n\n$$\n\\int_0^1 x\n$$\n\nend\n";
10643        // Any line of the block reveals the whole of it: here the middle one.
10644        let mid = src.find("\\int").unwrap();
10645        let line = mid..mid + "\\int_0^1 x".len();
10646        let m = map_math(src, Format::Markdown, &pictures(), Some(Reveal::math(line)));
10647        assert_eq!(
10648            row_texts(&m),
10649            vec!["intro", "", "$$", "\\int_0^1 x", "$$", "", "end"]
10650        );
10651        assert!(m.math.is_empty());
10652        let fence = m.rows[2].glyphs.iter().find(|g| g.ch == '$').unwrap();
10653        assert_eq!(fence.style.role, Role::Delimiter);
10654        assert_eq!(fence.src, 7);
10655        let x = m.rows[3].glyphs.iter().find(|g| g.ch == 'x').unwrap();
10656        assert_eq!(x.style.role, Role::Code);
10657        assert_eq!(x.src, src.find("x\n$$").unwrap());
10658        // Every byte of the source is reachable: a stop on each fence and each
10659        // character between.
10660        assert!(m.is_stop(7));
10661        assert!(m.is_stop(mid));
10662        // The closing fence's line too, and the same for djot.
10663        let close = src.rfind("$$").unwrap();
10664        let m = map_math(
10665            src,
10666            Format::Markdown,
10667            &pictures(),
10668            Some(Reveal::math(close..close + 2)),
10669        );
10670        assert_eq!(row_texts(&m)[2], "$$");
10671        let src = "$$`\n\\int\n`\n";
10672        let m = map_math(src, Format::Djot, &pictures(), Some(Reveal::math(4..8)));
10673        assert_eq!(row_texts(&m), vec!["$$`", "\\int", "`"]);
10674    }
10675
10676    #[test]
10677    fn a_block_that_holds_a_formula_says_so_to_the_cache() {
10678        let src = "plain\n\nwith $x$ in it\n\n$$\ny\n$$\n";
10679        let mut ed = Editor::new_ext(
10680            src.as_bytes(),
10681            Format::Markdown,
10682            crate::doc::parse_extensions(),
10683        )
10684        .unwrap();
10685        let mut cache = BlockCache::default();
10686        let top = top_blocks(&mut ed);
10687        let _ = build_cached(
10688            &top,
10689            src,
10690            None,
10691            false,
10692            &pictures(),
10693            None,
10694            &mut cache,
10695            |id| ed.subtree(NodeId(id)).unwrap_or_default(),
10696        );
10697        assert!(!cache.math_meets(&(0..5)), "the plain paragraph");
10698        assert!(cache.math_meets(&(7..21)), "the one with an atom");
10699        assert!(
10700            cache.math_meets(&(26..27)),
10701            "the middle line of the display block"
10702        );
10703        // A hit carries the answer without a walk: build again from the cache.
10704        let _ = build_cached(
10705            &top,
10706            src,
10707            None,
10708            false,
10709            &pictures(),
10710            None,
10711            &mut cache,
10712            |_| Vec::new(),
10713        );
10714        assert!(cache.math_meets(&(7..21)));
10715        assert!(!cache.math_meets(&(0..5)));
10716    }
10717
10718    #[test]
10719    fn incremental_builds_agree_with_the_reference_on_math() {
10720        for src in [
10721            "a $x$ b\n\n$$\ny\n$$\n\nc\n",
10722            "one\n\ntwo $\\frac{a}{b}$ three\n",
10723            "$$\n\\int\n$$\n",
10724        ] {
10725            for surface in [Surface::default(), pictures()] {
10726                let mut ed = Editor::new_ext(
10727                    src.as_bytes(),
10728                    Format::Markdown,
10729                    crate::doc::parse_extensions(),
10730                )
10731                .unwrap();
10732                let all = ed.nodes().unwrap();
10733                let plain = build(&all, src, Some(80), false, &surface, None);
10734                let mut cache = BlockCache::default();
10735                let top = top_blocks(&mut ed);
10736                let cached = build_cached(
10737                    &top,
10738                    src,
10739                    Some(80),
10740                    false,
10741                    &surface,
10742                    None,
10743                    &mut cache,
10744                    |id| ed.subtree(NodeId(id)).unwrap_or_default(),
10745                );
10746                assert_eq!(row_texts(&plain), row_texts(&cached), "{src:?}");
10747                assert_eq!(plain.math, cached.math, "{src:?}");
10748                assert_eq!(plain.stops, cached.stops, "{src:?}");
10749            }
10750        }
10751    }
10752}