Skip to main content

common/
format_runs.rs

1//! Per-block character formatting as sorted, non-overlapping byte spans.
2//!
3//! Each block carries a `Vec<FormatRun>` (formatting) and a
4//! `Vec<ImageAnchor>` (image positions). The block's `plain_text` is
5//! the authoritative character source for byte offsets used by both.
6//! Replaces the pre-Phase-1 model where every formatted run and every
7//! inline image was a row in the now-deleted `inline_elements` entity
8//! table; the [`InlineSegment`] type in this module is a transient
9//! view synthesized from `(plain_text, format_runs, block_images)`
10//! for readers (export, fragments, cursor) that still consume a
11//! per-segment shape.
12//!
13//! Invariants are documented on [`FormatRun`] and enforced by
14//! [`debug_assert_well_formed`] and by [`splice_range`] / [`shift_after`]
15//! which rebuild the run list while preserving them.
16//!
17//! **In a release build a `debug_assert!` is not enforcement.** It is compiled out, so
18//! a contract violation there produced a malformed run list *silently* — and autosave
19//! wrote that corruption to the writer's file seconds later. The bulk-edit path
20//! therefore uses the checked siblings, which report instead of assert:
21//! [`check_well_formed`], [`try_splice_range`] and [`shift_runs_for_replace`], all
22//! returning [`FormatRunError`].
23
24use crate::entities::{CharVerticalAlignment, UnderlineStyle};
25use serde::{Deserialize, Serialize};
26use thiserror::Error;
27
28/// A violation of the format-run invariants, reported instead of asserted.
29///
30/// The invariants used to be guarded only by `debug_assert!`, which is **compiled out
31/// of release builds** — so a contract violation silently produced a malformed run list
32/// in a shipped binary, and (with autosave) that corruption became the file's new truth
33/// within seconds. Anything on the *replace* path returns this instead, so the use case
34/// can refuse the edit and say why rather than quietly mangle a writer's formatting.
35#[derive(Debug, Clone, PartialEq, Eq, Error)]
36pub enum FormatRunError {
37    #[error("byte range {start}..{end} is reversed")]
38    ReversedRange { start: u32, end: u32 },
39
40    #[error(
41        "replacement run {run_start}..{run_end} falls outside the spliced range \
42         {range_start}..{range_end}"
43    )]
44    ReplacementOutsideRange {
45        run_start: u32,
46        run_end: u32,
47        range_start: u32,
48        range_end: u32,
49    },
50
51    #[error("run {start}..{end} is empty or reversed")]
52    EmptyRun { start: u32, end: u32 },
53
54    #[error("runs overlap or are out of order at index {index}: {left:?} then {right:?}")]
55    RunsOverlap {
56        index: usize,
57        left: Box<FormatRun>,
58        right: Box<FormatRun>,
59    },
60
61    #[error("adjacent runs with identical formatting were left uncoalesced at index {index}")]
62    RunsNotCoalesced { index: usize },
63
64    #[error("run {start}..{end} runs past the end of the block's {text_len} bytes")]
65    RunPastEndOfBlock {
66        start: u32,
67        end: u32,
68        text_len: usize,
69    },
70}
71
72/// What the replacement text wears when it overwrites formatted text.
73///
74/// Before this existed the behaviour was **emergent, not chosen**: a replace was a
75/// delete followed by an insert, and `shift_runs_for_insert` hardcodes "the inserted
76/// text inherits whatever run ends at the insertion point". That silently destroys
77/// formatting — renaming a character whose name reads `Auré**lien**` dropped the bold
78/// entirely, and not one test in the repo covered it.
79///
80/// The old behaviour is still the default (and is byte-for-byte identical); it is now
81/// simply one option among four, and the caller has to look at it.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
83pub enum ReplaceFormatPolicy {
84    /// The replacement inherits the format of the run that straddles the start of the
85    /// replaced range or ends exactly on it (precisely: `byte_start < start &&
86    /// byte_end >= start`); otherwise it is unformatted. A run beginning *exactly* at
87    /// the start never contributes.
88    ///
89    /// This is the Qt / ProseMirror insertion convention, and it is what
90    /// delete-then-insert has always produced — byte for byte, which
91    /// `inherit_preceding_matches_the_historical_delete_then_insert` pins *differentially*
92    /// (it runs the historical primitives and compares, rather than trusting a
93    /// transcription of them).
94    #[default]
95    InheritPreceding,
96
97    /// If a **single run covers the whole replaced range**, the replacement keeps that
98    /// run's format; otherwise fall back to [`Self::InheritPreceding`]. "Rename a name
99    /// that was entirely bold and it stays bold" — without guessing when the range is
100    /// formatted unevenly.
101    PreserveIfFullyCovered,
102
103    /// The replacement takes the format that covered the **most bytes** of the replaced
104    /// range. Unformatted gaps count as a candidate, so a mostly-plain range stays
105    /// plain; ties go to the *formatted* run, because a tie means the writer's
106    /// formatting covered at least as much as its absence and dropping it is the
107    /// destructive answer.
108    KeepDominantRun,
109
110    /// The replacement carries no formatting at all.
111    PreserveNothing,
112}
113
114/// Content type for an inline segment: text, image, footnote reference, or empty.
115#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
116pub enum InlineContent {
117    #[default]
118    Empty,
119    Text(String),
120    /// A footnote reference — the marker in the prose that points at a note.
121    ///
122    /// Carries only the **label**, never the number a reader sees. The number is
123    /// a fact about document order and about which notes an export happens to
124    /// include, so storing it would mean rewriting the author's prose every time
125    /// a note was inserted above it. Renderers derive it; the model never knows
126    /// it.
127    ///
128    /// The label need not resolve. A reference whose definition lives outside
129    /// this document — which is the normal state for a host that owns note
130    /// bodies itself — must survive a round trip unchanged, so nothing here
131    /// requires a matching definition to exist.
132    FootnoteRef {
133        label: String,
134    },
135    Image {
136        name: String,
137        /// Alternative text describing the image.
138        ///
139        /// Carried on the model rather than derived from the resource name
140        /// because it is the image's accessible description and its export
141        /// representation (HTML/EPUB `alt`, DOCX drawing description, Djot's
142        /// `![…]` label) — a filename is none of those things.
143        ///
144        /// `#[serde(default)]`: fragments are serialized onto the OS clipboard,
145        /// so a payload written by a build without this field must still
146        /// deserialize.
147        #[serde(default)]
148        alt: String,
149        width: i64,
150        height: i64,
151        quality: i64,
152    },
153}
154
155/// A lean view type representing one inline segment (text or image) with its
156/// associated formatting. Used by readers (export, fragments, cursor) to
157/// consume per-segment data synthesized from `(plain_text, format_runs,
158/// block_images)` via [`crate::format_runs_query::inline_segments_for_block`].
159/// Never stored — synthesized on demand.
160///
161/// The `fmt_*` field names match those on `Block` and on `FragmentElement`
162/// so readers can copy fields verbatim across the three types.
163#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
164pub struct InlineSegment {
165    pub content: InlineContent,
166    pub fmt_font_family: Option<String>,
167    pub fmt_font_point_size: Option<i64>,
168    pub fmt_font_weight: Option<i64>,
169    pub fmt_font_bold: Option<bool>,
170    pub fmt_font_italic: Option<bool>,
171    pub fmt_font_underline: Option<bool>,
172    pub fmt_font_overline: Option<bool>,
173    pub fmt_font_strikeout: Option<bool>,
174    pub fmt_letter_spacing: Option<i64>,
175    pub fmt_word_spacing: Option<i64>,
176    pub fmt_anchor_href: Option<String>,
177    pub fmt_anchor_names: Vec<String>,
178    pub fmt_is_anchor: Option<bool>,
179    pub fmt_tooltip: Option<String>,
180    pub fmt_underline_style: Option<UnderlineStyle>,
181    pub fmt_vertical_alignment: Option<CharVerticalAlignment>,
182}
183
184/// Character-level formatting for a contiguous byte span. One per
185/// [`FormatRun`]; one per [`ImageAnchor`]. Fields mirror the `fmt_*`
186/// set on [`InlineSegment`] and on `FragmentElement` so values copy
187/// across types verbatim.
188#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
189pub struct CharacterFormat {
190    pub font_family: Option<String>,
191    pub font_point_size: Option<i64>,
192    pub font_weight: Option<i64>,
193    pub font_bold: Option<bool>,
194    pub font_italic: Option<bool>,
195    pub font_underline: Option<bool>,
196    pub font_overline: Option<bool>,
197    pub font_strikeout: Option<bool>,
198    pub letter_spacing: Option<i64>,
199    pub word_spacing: Option<i64>,
200    pub anchor_href: Option<String>,
201    pub anchor_names: Vec<String>,
202    pub is_anchor: Option<bool>,
203    pub tooltip: Option<String>,
204    pub underline_style: Option<UnderlineStyle>,
205    pub vertical_alignment: Option<CharVerticalAlignment>,
206}
207
208/// One run of identical character formatting inside a block. Byte offsets
209/// are relative to the block's `plain_text` (Phase 1) or to the block's
210/// rope range (Phase 2).
211#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
212pub struct FormatRun {
213    pub byte_start: u32,
214    pub byte_end: u32,
215    pub format: CharacterFormat,
216}
217
218/// An image embedded at a specific byte position inside a block. In
219/// Phase 1 the byte position is an index into the block's `plain_text`;
220/// in Phase 2 it points at the U+FFFC sentinel character in the rope.
221///
222/// Images carry their own [`CharacterFormat`] because vertical alignment
223/// and anchor metadata apply per inline run.
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225pub struct ImageAnchor {
226    pub byte_offset: u32,
227    pub name: String,
228    /// Alternative text. See [`InlineContent::Image::alt`] for why it lives on
229    /// the model and why it defaults.
230    #[serde(default)]
231    pub alt: String,
232    pub width: i64,
233    pub height: i64,
234    pub quality: i64,
235    pub format: CharacterFormat,
236}
237
238/// A footnote reference embedded at a specific byte position inside a block.
239///
240/// The exact shape of [`ImageAnchor`], and for the same reason: a reference is
241/// an inline object occupying one `U+FFFC` sentinel in the block's text, so the
242/// rope moves it with every edit and deleting the sentence deletes the
243/// reference — no re-anchoring, no quote matching, no orphan state to keep.
244///
245/// It carries its own [`CharacterFormat`] because a reference is normally
246/// superscript, and because whatever formatting surrounds it should not bleed
247/// onto the marker.
248///
249/// What it deliberately does **not** carry is the number. See
250/// [`InlineContent::FootnoteRef`].
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct FootnoteRefAnchor {
253    pub byte_offset: u32,
254    /// Matches a footnote definition's `Frame::footnote_label`, when one exists
255    /// in this document at all. Djot's `[^label]` syntax on the wire.
256    pub label: String,
257    pub format: CharacterFormat,
258}
259
260/// One inline object anchored in a block: an image, or a footnote reference.
261///
262/// Both occupy a single `U+FFFC` and are woven into the text by the same rules,
263/// so the weave takes them as one byte-ordered sequence rather than walking two
264/// lists and hoping they interleave. They are stored apart because they are
265/// edited apart, and are brought together only here.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum BlockAnchor<'a> {
268    Image(&'a ImageAnchor),
269    FootnoteRef(&'a FootnoteRefAnchor),
270}
271
272impl BlockAnchor<'_> {
273    /// Byte position of this anchor's sentinel within the block's text.
274    pub fn byte_offset(&self) -> u32 {
275        match self {
276            BlockAnchor::Image(i) => i.byte_offset,
277            BlockAnchor::FootnoteRef(f) => f.byte_offset,
278        }
279    }
280}
281
282/// Every anchor in a block, in byte order.
283///
284/// A stable sort, so an image and a reference mirrored at the same offset keep
285/// a deterministic order rather than swapping between reads — which would show
286/// up as a diff in exported Djot for a document nobody edited.
287pub fn block_anchors<'a>(
288    images: &'a [ImageAnchor],
289    footnote_refs: &'a [FootnoteRefAnchor],
290) -> Vec<BlockAnchor<'a>> {
291    let mut anchors: Vec<BlockAnchor<'a>> = Vec::with_capacity(images.len() + footnote_refs.len());
292    anchors.extend(images.iter().map(BlockAnchor::Image));
293    anchors.extend(footnote_refs.iter().map(BlockAnchor::FootnoteRef));
294    anchors.sort_by_key(|a| a.byte_offset());
295    anchors
296}
297
298/// Debug-only invariant check. Run from `debug_assert!` callsites in
299/// the use cases that mutate format runs. Cheap: O(n) where n is the
300/// run count (typically < 100 per block in real prose).
301///
302/// # Invariants
303/// 1. Runs are sorted by `byte_start` ascending.
304/// 2. Each run has `byte_start < byte_end`.
305/// 3. Runs are non-overlapping: `runs[i].byte_end <= runs[i+1].byte_start`.
306/// 4. The last run's `byte_end` does not exceed `block_text_len`.
307/// 5. Adjacent runs with identical format are coalesced (no two
308///    consecutive runs satisfy `byte_end == next.byte_start &&
309///    format == next.format`).
310pub fn debug_assert_well_formed(runs: &[FormatRun], block_text_len: usize) {
311    // Only pay the O(n) walk where the assertion would actually fire.
312    if cfg!(debug_assertions)
313        && let Err(e) = check_well_formed(runs, block_text_len)
314    {
315        debug_assert!(false, "format runs are malformed: {e}");
316    }
317}
318
319/// The same invariants as [`debug_assert_well_formed`], but **reported rather than
320/// asserted** — so a caller that must not corrupt a block can refuse the edit.
321///
322/// This exists because `debug_assert!` is compiled out of release builds: a malformed
323/// run list produced in a shipped binary went completely undetected, and autosave wrote
324/// it to disk seconds later. The replace path calls this and propagates the error.
325pub fn check_well_formed(runs: &[FormatRun], block_text_len: usize) -> Result<(), FormatRunError> {
326    if runs.is_empty() {
327        return Ok(());
328    }
329    for run in runs {
330        if run.byte_start >= run.byte_end {
331            return Err(FormatRunError::EmptyRun {
332                start: run.byte_start,
333                end: run.byte_end,
334            });
335        }
336    }
337    for i in 0..runs.len() - 1 {
338        if runs[i].byte_end > runs[i + 1].byte_start {
339            return Err(FormatRunError::RunsOverlap {
340                index: i,
341                left: Box::new(runs[i].clone()),
342                right: Box::new(runs[i + 1].clone()),
343            });
344        }
345        if runs[i].byte_end == runs[i + 1].byte_start && runs[i].format == runs[i + 1].format {
346            return Err(FormatRunError::RunsNotCoalesced { index: i });
347        }
348    }
349    let last = runs.last().expect("non-empty");
350    if last.byte_end as usize > block_text_len {
351        return Err(FormatRunError::RunPastEndOfBlock {
352            start: last.byte_start,
353            end: last.byte_end,
354            text_len: block_text_len,
355        });
356    }
357    Ok(())
358}
359
360/// Merge adjacent runs that have identical formatting. O(n).
361pub fn coalesce_in_place(runs: &mut Vec<FormatRun>) {
362    if runs.len() < 2 {
363        return;
364    }
365    let mut write = 0usize;
366    for read in 1..runs.len() {
367        if runs[write].byte_end == runs[read].byte_start && runs[write].format == runs[read].format
368        {
369            runs[write].byte_end = runs[read].byte_end;
370        } else {
371            write += 1;
372            if write != read {
373                runs[write] = runs[read].clone();
374            }
375        }
376    }
377    runs.truncate(write + 1);
378}
379
380/// Replace the runs covering `range` with `replacement`, preserving the
381/// invariants. Runs that straddle the range boundary are clipped on
382/// either side; runs fully contained are removed.
383///
384/// The replacement byte ranges must lie within `range` and themselves
385/// be well-formed (sorted, non-overlapping). The function does NOT
386/// shift bytes after `range.end` — callers wanting to splice in a
387/// different-length text must call [`shift_after`] first or after,
388/// depending on whether the text length is changing.
389pub fn splice_range(
390    runs: &mut Vec<FormatRun>,
391    range: std::ops::Range<u32>,
392    replacement: Vec<FormatRun>,
393) {
394    if let Err(e) = try_splice_range(runs, range, replacement) {
395        // Previously this was a bare `debug_assert!` — compiled OUT of release, where a
396        // contract violation therefore went on to build a malformed run list and corrupt
397        // the block's formatting silently. Keep the loud failure in debug; in release,
398        // refuse the splice and leave `runs` untouched rather than mangle it.
399        //
400        // No existing caller can reach this: every one either builds a replacement that
401        // spans exactly the range, or goes through `shift_runs_for_delete`, which
402        // early-returns on an inverted range. It is a net, not a behaviour change.
403        debug_assert!(false, "splice_range contract violated: {e}");
404    }
405}
406
407/// [`splice_range`], but the contract is **checked and reported** instead of asserted.
408///
409/// `runs` is left completely untouched when this returns `Err` — validation happens
410/// before any mutation, so a rejected splice cannot half-apply.
411pub fn try_splice_range(
412    runs: &mut Vec<FormatRun>,
413    range: std::ops::Range<u32>,
414    replacement: Vec<FormatRun>,
415) -> Result<(), FormatRunError> {
416    if range.start > range.end {
417        return Err(FormatRunError::ReversedRange {
418            start: range.start,
419            end: range.end,
420        });
421    }
422    for r in &replacement {
423        if r.byte_start >= r.byte_end {
424            return Err(FormatRunError::EmptyRun {
425                start: r.byte_start,
426                end: r.byte_end,
427            });
428        }
429        if r.byte_start < range.start || r.byte_end > range.end {
430            return Err(FormatRunError::ReplacementOutsideRange {
431                run_start: r.byte_start,
432                run_end: r.byte_end,
433                range_start: range.start,
434                range_end: range.end,
435            });
436        }
437    }
438    for i in 1..replacement.len() {
439        if replacement[i - 1].byte_end > replacement[i].byte_start {
440            return Err(FormatRunError::RunsOverlap {
441                index: i - 1,
442                left: Box::new(replacement[i - 1].clone()),
443                right: Box::new(replacement[i].clone()),
444            });
445        }
446    }
447
448    let mut result: Vec<FormatRun> = Vec::with_capacity(runs.len() + replacement.len());
449
450    // Keep / clip everything strictly before range.start.
451    for run in runs.iter() {
452        if run.byte_end <= range.start {
453            result.push(run.clone());
454        } else if run.byte_start < range.start {
455            // Run straddles range.start: keep the left part.
456            result.push(FormatRun {
457                byte_start: run.byte_start,
458                byte_end: range.start,
459                format: run.format.clone(),
460            });
461        }
462    }
463
464    // Insert the replacement runs.
465    result.extend(replacement);
466
467    // Keep / clip everything starting at or after range.end.
468    for run in runs.iter() {
469        if run.byte_start >= range.end {
470            result.push(run.clone());
471        } else if run.byte_end > range.end {
472            // Run straddles range.end: keep the right part.
473            result.push(FormatRun {
474                byte_start: range.end,
475                byte_end: run.byte_end,
476                format: run.format.clone(),
477            });
478        }
479    }
480
481    coalesce_in_place(&mut result);
482    *runs = result;
483    Ok(())
484}
485
486/// Capture the slice of `runs` that intersects `[start..end)`, clipped
487/// to those bounds. Used by hand-rolled-inverse undo for format-only
488/// edits: callers capture this BEFORE calling [`splice_range`], and
489/// on undo splice the captured runs back into the same byte range to
490/// restore the prior state without paying the cost of a full
491/// `RopeStoreSnapshot`.
492///
493/// Gaps in the original runs (positions inside `[start..end)` with no
494/// formatting) become gaps in the captured output too — the undo
495/// splice preserves them faithfully.
496pub fn capture_runs_in_range(runs: &[FormatRun], start: u32, end: u32) -> Vec<FormatRun> {
497    let mut out = Vec::new();
498    for run in runs {
499        if run.byte_end <= start || run.byte_start >= end {
500            continue;
501        }
502        let clipped_start = std::cmp::max(run.byte_start, start);
503        let clipped_end = std::cmp::min(run.byte_end, end);
504        if clipped_start < clipped_end {
505            out.push(FormatRun {
506                byte_start: clipped_start,
507                byte_end: clipped_end,
508                format: run.format.clone(),
509            });
510        }
511    }
512    out
513}
514
515/// Capture the `(byte_offset, format)` pairs for every image anchor
516/// inside `[start..end)`. Used together with [`capture_runs_in_range`]
517/// by hand-rolled-inverse undo for format-only edits.
518pub fn capture_image_formats_in_range(
519    images: &[ImageAnchor],
520    start: u32,
521    end: u32,
522) -> Vec<(u32, CharacterFormat)> {
523    let mut out = Vec::new();
524    for img in images {
525        if img.byte_offset >= start && img.byte_offset < end {
526            out.push((img.byte_offset, img.format.clone()));
527        }
528    }
529    out
530}
531
532/// Shift the byte offsets of every run whose `byte_start >= threshold`
533/// by `delta`. Used after a text insert/delete to keep downstream runs
534/// in sync with the new block text. Runs strictly before the threshold
535/// are unaffected; runs that straddle the threshold are left alone
536/// (the caller should have spliced them first).
537///
538/// Panics in debug mode if `delta` would underflow a run's offset.
539pub fn shift_after(runs: &mut [FormatRun], threshold: u32, delta: i32) {
540    for run in runs.iter_mut() {
541        if run.byte_start >= threshold {
542            let new_start = (run.byte_start as i64) + (delta as i64);
543            let new_end = (run.byte_end as i64) + (delta as i64);
544            debug_assert!(new_start >= 0 && new_end >= new_start);
545            run.byte_start = new_start as u32;
546            run.byte_end = new_end as u32;
547        }
548    }
549}
550
551/// Synthesize a stable per-fragment id from a block id and byte offset
552/// within that block. Populates the `element_id` field in
553/// `FragmentContent::{Text, Image}` (the public layout-engine type),
554/// giving callers a stable handle across renders even though the
555/// underlying [`InlineSegment`]s are never stored. Two segments at the
556/// same `(block_id, byte_start)` always produce the same id; a segment
557/// that moves to a new byte_start (e.g. due to an insert upstream)
558/// gets a new id.
559///
560/// Bit layout (u64): bit 62 = synth tag (so synthesized ids never
561/// collide with real entity ids issued by the store's counter, which
562/// start at 1 and grow upward). Bits 32..62 = block id (1 billion
563/// blocks per document, 30 bits). Bottom 32 bits = byte offset (4 GB
564/// per block). The top bit stays zero so the value fits in positive
565/// i64 range — public DTOs expose element_id as i64.
566pub fn synth_element_id(block_id: u64, byte_start: u32) -> u64 {
567    const SYNTH_TAG: u64 = 0x4000_0000_0000_0000;
568    SYNTH_TAG | ((block_id & 0x3FFF_FFFF) << 32) | (byte_start as u64)
569}
570
571/// Same as `shift_after` for image anchors. Anchors AT the threshold are
572/// shifted (treated as part of the inserted region's right side).
573pub fn shift_images_after(images: &mut [ImageAnchor], threshold: u32, delta: i32) {
574    for img in images.iter_mut() {
575        if img.byte_offset >= threshold {
576            let new_off = (img.byte_offset as i64) + (delta as i64);
577            debug_assert!(new_off >= 0);
578            img.byte_offset = new_off as u32;
579        }
580    }
581}
582
583// ─────────────────────────────────────────────────────────────────────
584// Composite helpers used by writer use cases. These keep the per-block
585// run / image vectors well-formed under insert / delete / split.
586// ─────────────────────────────────────────────────────────────────────
587
588/// Apply an "insert `inserted_bytes` of text at `byte_offset`" mutation
589/// to a block's runs in place. Runs strictly before the offset are
590/// unchanged; runs strictly after are shifted by +inserted_bytes; runs
591/// that straddle the offset are extended (the inserted text inherits
592/// the surrounding run's format — Qt / ProseMirror convention).
593pub fn shift_runs_for_insert(runs: &mut [FormatRun], byte_offset: u32, inserted_bytes: u32) {
594    if inserted_bytes == 0 {
595        return;
596    }
597    for run in runs.iter_mut() {
598        if run.byte_start >= byte_offset {
599            run.byte_start += inserted_bytes;
600            run.byte_end += inserted_bytes;
601        } else if run.byte_end >= byte_offset {
602            // Run straddles the insertion point, or its right edge sits
603            // exactly on it. In both cases the inserted text inherits
604            // this run's format (Qt convention).
605            run.byte_end += inserted_bytes;
606        }
607    }
608}
609
610/// Apply a "delete byte range `[byte_start..byte_end)`" mutation to a
611/// block's runs. Splices the range with empty replacement (clipping
612/// straddling runs) and shifts everything past `byte_end` back by the
613/// deleted length. Adjacent runs that end up equal-format are coalesced.
614pub fn shift_runs_for_delete(runs: &mut Vec<FormatRun>, byte_start: u32, byte_end: u32) {
615    if byte_end <= byte_start {
616        return;
617    }
618    splice_range(runs, byte_start..byte_end, Vec::new());
619    let delta = (byte_end - byte_start) as i32;
620    shift_after(runs, byte_end, -delta);
621    // The shift can make a left-clipped run abut a shifted trailing run
622    // with identical format; coalesce once more to restore the invariant.
623    coalesce_in_place(runs);
624}
625
626/// Apply a "replace byte range `[byte_start..byte_end)` with `replacement_bytes` bytes"
627/// mutation to a block's runs, choosing explicitly what the replacement wears.
628///
629/// This is the one edit where the old delete-then-insert composition quietly lost a
630/// writer's formatting: replacing `Auré**lien**` deletes both runs under the range and
631/// then lets the replacement inherit whatever preceded it, so the bold is gone. That
632/// behaviour is still available — and is still the default — but it is now a *decision*
633/// ([`ReplaceFormatPolicy`]) rather than an accident of two functions being called in
634/// sequence.
635///
636/// Additive by design: `shift_runs_for_delete` / `shift_runs_for_insert` are untouched,
637/// because 13 other call sites across delete/insert/paste depend on their exact
638/// behaviour. [`ReplaceFormatPolicy::InheritPreceding`] is implemented *by calling that
639/// very composition*, so the default cannot drift away from what shipped.
640///
641/// Returns `Err` rather than corrupting the block if the range is inverted or the
642/// re-splice would violate the run invariants — see [`FormatRunError`].
643pub fn shift_runs_for_replace(
644    runs: &mut Vec<FormatRun>,
645    byte_start: u32,
646    byte_end: u32,
647    replacement_bytes: u32,
648    policy: ReplaceFormatPolicy,
649) -> Result<(), FormatRunError> {
650    if byte_end < byte_start {
651        return Err(FormatRunError::ReversedRange {
652            start: byte_start,
653            end: byte_end,
654        });
655    }
656
657    // Decide what the replacement wears from the runs as they stand BEFORE the edit —
658    // afterwards the runs under the range are gone and the evidence with them.
659    //
660    // `None` here means "do not override": let the composition below decide, which is
661    // exactly `InheritPreceding`.
662    //
663    // An empty range replaces nothing, so it is an *insertion*, and the two
664    // coverage-based policies have nothing to reason about — they must defer to the
665    // insert convention rather than strip the format the typed text would have
666    // inherited. Only `PreserveNothing`, which is an explicit request for unformatted
667    // text, still applies.
668    let destroys_formatting = byte_end > byte_start;
669    let override_format: Option<Option<CharacterFormat>> = match policy {
670        ReplaceFormatPolicy::InheritPreceding => None,
671        ReplaceFormatPolicy::PreserveNothing => Some(None),
672        ReplaceFormatPolicy::PreserveIfFullyCovered => {
673            covering_format(runs, byte_start, byte_end).map(Some)
674        }
675        ReplaceFormatPolicy::KeepDominantRun if destroys_formatting => {
676            Some(dominant_format(runs, byte_start, byte_end))
677        }
678        ReplaceFormatPolicy::KeepDominantRun => None,
679    };
680
681    // The historical composition. This IS `InheritPreceding`, byte for byte.
682    shift_runs_for_delete(runs, byte_start, byte_end);
683    shift_runs_for_insert(runs, byte_start, replacement_bytes);
684
685    // Every other policy is a targeted re-splice of exactly the replacement's span.
686    if let Some(format) = override_format
687        && replacement_bytes > 0
688    {
689        let span = byte_start..byte_start + replacement_bytes;
690        let replacement = match format {
691            Some(format) => vec![FormatRun {
692                byte_start: span.start,
693                byte_end: span.end,
694                format,
695            }],
696            None => Vec::new(),
697        };
698        try_splice_range(runs, span, replacement)?;
699    }
700    Ok(())
701}
702
703/// The format of the single run covering **all** of `[start..end)`, if one does.
704///
705/// An empty range is covered by nothing: a pure insertion has no formatting of its own
706/// to preserve, so every policy falls back to inheritance there rather than silently
707/// formatting typed text from a run it merely sits next to.
708fn covering_format(runs: &[FormatRun], start: u32, end: u32) -> Option<CharacterFormat> {
709    if end <= start {
710        return None;
711    }
712    runs.iter()
713        .find(|r| r.byte_start <= start && r.byte_end >= end)
714        .map(|r| r.format.clone())
715}
716
717/// The format covering the most bytes of `[start..end)`, or `None` if unformatted text
718/// covers more than any single run.
719///
720/// Gaps count as a candidate, so renaming inside a mostly-plain range stays plain. Ties
721/// go to the formatted run: a tie means the formatting covered at least as much of the
722/// range as its absence did, and dropping it is the destructive outcome.
723fn dominant_format(runs: &[FormatRun], start: u32, end: u32) -> Option<CharacterFormat> {
724    if end <= start {
725        return None;
726    }
727    let span = u64::from(end - start);
728    let mut covered = 0u64;
729    let mut best: Option<(u64, &FormatRun)> = None;
730
731    for r in runs {
732        let lo = r.byte_start.max(start);
733        let hi = r.byte_end.min(end);
734        if hi <= lo {
735            continue;
736        }
737        let overlap = u64::from(hi - lo);
738        covered += overlap;
739        // `>` keeps the EARLIEST run on a tie between two runs, so the result does not
740        // depend on iteration order beyond the list's own sortedness.
741        if best.is_none_or(|(best_overlap, _)| overlap > best_overlap) {
742            best = Some((overlap, r));
743        }
744    }
745
746    let plain = span - covered;
747    match best {
748        Some((overlap, run)) if overlap >= plain => Some(run.format.clone()),
749        _ => None,
750    }
751}
752
753/// Apply an "insert" shift to a block's image anchors. Anchors at or
754/// past the offset move forward by `inserted_bytes`.
755/// Apply an "insert" mutation to a block's footnote references — the rule
756/// [`shift_images_for_insert`] applies, for the same reason.
757pub fn shift_footnote_refs_for_insert(
758    notes: &mut [FootnoteRefAnchor],
759    byte_offset: u32,
760    inserted_bytes: u32,
761) {
762    if inserted_bytes == 0 {
763        return;
764    }
765    for note in notes.iter_mut() {
766        if note.byte_offset >= byte_offset {
767            note.byte_offset += inserted_bytes;
768        }
769    }
770}
771
772pub fn shift_images_for_insert(images: &mut [ImageAnchor], byte_offset: u32, inserted_bytes: u32) {
773    if inserted_bytes == 0 {
774        return;
775    }
776    for img in images.iter_mut() {
777        if img.byte_offset >= byte_offset {
778            img.byte_offset += inserted_bytes;
779        }
780    }
781}
782
783/// Apply a "delete" mutation to a block's image anchors. Anchors whose
784/// `byte_offset` falls inside `[byte_start..byte_end)` are removed;
785/// anchors at or past `byte_end` shift back by the deleted length.
786/// Returns the number of anchors removed.
787pub fn shift_images_for_delete(
788    images: &mut Vec<ImageAnchor>,
789    byte_start: u32,
790    byte_end: u32,
791) -> usize {
792    if byte_end <= byte_start {
793        return 0;
794    }
795    let before = images.len();
796    images.retain(|i| !(i.byte_offset >= byte_start && i.byte_offset < byte_end));
797    let removed = before - images.len();
798    let delta = (byte_end - byte_start) as i32;
799    shift_images_after(images, byte_end, -delta);
800    removed
801}
802
803/// Translate a logical character offset (counting text characters AND
804/// image positions interleaved by their `byte_offset`) into a UTF-8
805/// byte offset within `plain_text`. Used by writer use cases to map a
806/// document-space char position to the byte position where text edits
807/// should land in `block.plain_text`.
808///
809/// Each image already occupies exactly one character of `plain_text`: the
810/// `U+FFFC` OBJECT REPLACEMENT CHARACTER that `insert_image` mirrors into the
811/// rope, which [`ImageAnchor::byte_offset`] points at. So a logical offset *is*
812/// a character offset and this is a plain char→byte mapping.
813///
814/// `images` is retained in the signature — and deliberately unused — because
815/// getting this wrong is silent and expensive, and callers pass it naturally.
816/// The previous implementation walked the anchor list *in addition to*
817/// `char_indices()`, so every image advanced the logical counter twice. That
818/// dates from the pre-rope model, where an anchor genuinely contributed no
819/// bytes; once the sentinel went into the rope the two representations started
820/// double-counting. The visible effects: selecting across an image returned the
821/// wrong text (an extra sentinel, a missing character), and deleting a range
822/// containing one removed too little.
823pub fn logical_offset_to_byte(plain_text: &str, _images: &[ImageAnchor], char_offset: i64) -> u32 {
824    if char_offset <= 0 {
825        return 0;
826    }
827    plain_text
828        .char_indices()
829        .nth(char_offset as usize)
830        .map(|(b, _)| b as u32)
831        .unwrap_or(plain_text.len() as u32)
832}
833
834/// Split a block's format runs at `byte_offset`. The returned right-hand
835/// vector has its run offsets re-based so they start at byte 0 of the
836/// new (right) block. Straddling runs are split with their `format`
837/// cloned to both halves.
838pub fn split_runs_at(runs: &[FormatRun], byte_offset: u32) -> (Vec<FormatRun>, Vec<FormatRun>) {
839    let mut left = Vec::new();
840    let mut right = Vec::new();
841    for run in runs {
842        if run.byte_end <= byte_offset {
843            left.push(run.clone());
844        } else if run.byte_start >= byte_offset {
845            right.push(FormatRun {
846                byte_start: run.byte_start - byte_offset,
847                byte_end: run.byte_end - byte_offset,
848                format: run.format.clone(),
849            });
850        } else {
851            left.push(FormatRun {
852                byte_start: run.byte_start,
853                byte_end: byte_offset,
854                format: run.format.clone(),
855            });
856            right.push(FormatRun {
857                byte_start: 0,
858                byte_end: run.byte_end - byte_offset,
859                format: run.format.clone(),
860            });
861        }
862    }
863    (left, right)
864}
865
866/// Split block image anchors at `byte_offset`. Anchors at exactly
867/// `byte_offset` go to the right half (rebased to offset 0).
868/// Split footnote references at `byte_offset`, rebasing the right-hand side to
869/// zero — the exact rule [`split_images_at`] applies, because a reference
870/// occupies a block the same way an image does.
871pub fn split_footnote_refs_at(
872    notes: &[FootnoteRefAnchor],
873    byte_offset: u32,
874) -> (Vec<FootnoteRefAnchor>, Vec<FootnoteRefAnchor>) {
875    let mut left = Vec::new();
876    let mut right = Vec::new();
877    for note in notes {
878        if note.byte_offset < byte_offset {
879            left.push(note.clone());
880        } else {
881            let mut new = note.clone();
882            new.byte_offset -= byte_offset;
883            right.push(new);
884        }
885    }
886    (left, right)
887}
888
889pub fn split_images_at(
890    images: &[ImageAnchor],
891    byte_offset: u32,
892) -> (Vec<ImageAnchor>, Vec<ImageAnchor>) {
893    let mut left = Vec::new();
894    let mut right = Vec::new();
895    for img in images {
896        if img.byte_offset < byte_offset {
897            left.push(img.clone());
898        } else {
899            let mut new = img.clone();
900            new.byte_offset -= byte_offset;
901            right.push(new);
902        }
903    }
904    (left, right)
905}
906
907// ─────────────────────────────────────────────────────────────────────
908// View synthesis: build a Vec<InlineSegment> from format_runs + images.
909// ─────────────────────────────────────────────────────────────────────
910
911/// Copy the `fmt_*` fields of an `InlineSegment` into a `CharacterFormat`.
912pub fn character_format_from_segment(seg: &InlineSegment) -> CharacterFormat {
913    CharacterFormat {
914        font_family: seg.fmt_font_family.clone(),
915        font_point_size: seg.fmt_font_point_size,
916        font_weight: seg.fmt_font_weight,
917        font_bold: seg.fmt_font_bold,
918        font_italic: seg.fmt_font_italic,
919        font_underline: seg.fmt_font_underline,
920        font_overline: seg.fmt_font_overline,
921        font_strikeout: seg.fmt_font_strikeout,
922        letter_spacing: seg.fmt_letter_spacing,
923        word_spacing: seg.fmt_word_spacing,
924        anchor_href: seg.fmt_anchor_href.clone(),
925        anchor_names: seg.fmt_anchor_names.clone(),
926        is_anchor: seg.fmt_is_anchor,
927        tooltip: seg.fmt_tooltip.clone(),
928        underline_style: seg.fmt_underline_style.clone(),
929        vertical_alignment: seg.fmt_vertical_alignment.clone(),
930    }
931}
932
933/// Apply a `CharacterFormat` onto an `InlineSegment`'s fmt_* fields.
934pub fn apply_character_format_to_segment(seg: &mut InlineSegment, fmt: &CharacterFormat) {
935    seg.fmt_font_family = fmt.font_family.clone();
936    seg.fmt_font_point_size = fmt.font_point_size;
937    seg.fmt_font_weight = fmt.font_weight;
938    seg.fmt_font_bold = fmt.font_bold;
939    seg.fmt_font_italic = fmt.font_italic;
940    seg.fmt_font_underline = fmt.font_underline;
941    seg.fmt_font_overline = fmt.font_overline;
942    seg.fmt_font_strikeout = fmt.font_strikeout;
943    seg.fmt_letter_spacing = fmt.letter_spacing;
944    seg.fmt_word_spacing = fmt.word_spacing;
945    seg.fmt_anchor_href = fmt.anchor_href.clone();
946    seg.fmt_anchor_names = fmt.anchor_names.clone();
947    seg.fmt_is_anchor = fmt.is_anchor;
948    seg.fmt_tooltip = fmt.tooltip.clone();
949    seg.fmt_underline_style = fmt.underline_style.clone();
950    seg.fmt_vertical_alignment = fmt.vertical_alignment.clone();
951}
952
953/// One ordered piece of a block's inline content: a run of text, or an image.
954///
955/// Produced by [`merge_runs_and_anchors`] and mapped by each caller into its own
956/// output type.
957#[derive(Debug, Clone, PartialEq)]
958pub enum InlinePiece<'a> {
959    /// Byte range `[start, end)` of the block's `plain_text`. `format` is
960    /// `None` for bytes no format run covers.
961    Text {
962        start: u32,
963        end: u32,
964        format: Option<&'a CharacterFormat>,
965    },
966    /// An image anchored at this position. Contributes one logical character
967    /// and zero bytes.
968    Image(&'a ImageAnchor),
969    /// A footnote reference anchored at this position. Contributes one logical
970    /// character and zero bytes, exactly as an image does.
971    FootnoteRef(&'a FootnoteRefAnchor),
972}
973
974/// Interleave a block's format runs and image anchors into one ordered stream.
975///
976/// A block stores three parallel things — the text bytes, a list of formatted
977/// byte ranges, and a list of image anchors keyed by byte offset — and every
978/// reader has to weave them back into document order. That weave is fiddly in
979/// exactly one place: an image anchored *inside* a formatted run has to split
980/// the run, emitting the text before it, then the image, then the rest of the
981/// run with the same format.
982///
983/// This function exists because that weave was previously written out twice, by
984/// hand, in two crates, and the two copies disagreed. The version behind
985/// `inline_segments_view` only checked `img.byte_offset < run.byte_start`, so an
986/// image inside a run was skipped by the run loop entirely and swept up by the
987/// trailing loop — landing **after every run in the block**. Insert a picture
988/// into the middle of a bold sentence and every exporter, and the fragment/copy
989/// path, moved it to the end of the paragraph.
990///
991/// Callers map the returned pieces to their own types; nobody re-derives the
992/// ordering.
993pub fn merge_runs_and_anchors<'a>(
994    plain_text: &str,
995    runs: &'a [FormatRun],
996    anchors: &[BlockAnchor<'a>],
997) -> Vec<InlinePiece<'a>> {
998    let text_len = plain_text.len() as u32;
999    let mut out: Vec<InlinePiece<'a>> = Vec::new();
1000    let mut img_iter = anchors.iter().peekable();
1001    // Highest byte offset already emitted. Never moves backwards.
1002    let mut cursor: u32 = 0;
1003
1004    /// Bytes an anchor occupies in `plain_text`.
1005    ///
1006    /// `insert_image` mirrors a `U+FFFC` OBJECT REPLACEMENT CHARACTER into the
1007    /// rope at the anchor's `byte_offset`, so an image's own character sits in
1008    /// the text and text resuming after the image must step over it. Emitting
1009    /// both the image piece *and* the sentinel yields the image twice — which
1010    /// is what made a selection across an image read back with a doubled
1011    /// sentinel and a missing following character. A footnote reference is
1012    /// mirrored the same way and steps over the same three bytes.
1013    ///
1014    /// Checked rather than assumed: `U+FFFC` also marks table anchors, and not
1015    /// every writer of an `ImageAnchor` mirrors one (the test harness writes
1016    /// anchors directly). An anchor without a sentinel is still handled, it
1017    /// simply consumes no bytes.
1018    fn sentinel_len(plain_text: &str, byte_offset: u32) -> u32 {
1019        let at = byte_offset as usize;
1020        if plain_text.len() >= at + 3 && plain_text.as_bytes()[at..at + 3] == [0xEF, 0xBF, 0xBC] {
1021            3
1022        } else {
1023            0
1024        }
1025    }
1026
1027    let push_text = |out: &mut Vec<InlinePiece<'a>>,
1028                     start: u32,
1029                     end: u32,
1030                     format: Option<&'a CharacterFormat>| {
1031        if start < end {
1032            out.push(InlinePiece::Text { start, end, format });
1033        }
1034    };
1035
1036    /// The piece an anchor contributes, whichever kind it is.
1037    fn piece<'a>(anchor: &BlockAnchor<'a>) -> InlinePiece<'a> {
1038        match *anchor {
1039            BlockAnchor::Image(i) => InlinePiece::Image(i),
1040            BlockAnchor::FootnoteRef(f) => InlinePiece::FootnoteRef(f),
1041        }
1042    }
1043
1044    for run in runs {
1045        // Anchors strictly before this run: unformatted gap text, then the anchor.
1046        while let Some(anchor) = img_iter.peek() {
1047            let at = anchor.byte_offset();
1048            if at >= run.byte_start {
1049                break;
1050            }
1051            push_text(&mut out, cursor, at, None);
1052            out.push(piece(anchor));
1053            cursor = cursor.max(at + sentinel_len(plain_text, at));
1054            img_iter.next();
1055        }
1056
1057        // Unformatted gap between the last emission and the run's start.
1058        push_text(&mut out, cursor, run.byte_start, None);
1059        cursor = cursor.max(run.byte_start);
1060
1061        // Anchors inside the run split it, keeping the run's format on both
1062        // sides. `<=` so an anchor sitting exactly on the run's end boundary is
1063        // consumed here rather than deferred — deferring it is what produced
1064        // the out-of-order emission described above.
1065        while let Some(anchor) = img_iter.peek() {
1066            let at = anchor.byte_offset();
1067            if at > run.byte_end {
1068                break;
1069            }
1070            push_text(&mut out, cursor, at, Some(&run.format));
1071            out.push(piece(anchor));
1072            cursor = cursor.max(at + sentinel_len(plain_text, at));
1073            img_iter.next();
1074        }
1075
1076        push_text(&mut out, cursor, run.byte_end, Some(&run.format));
1077        cursor = cursor.max(run.byte_end);
1078    }
1079
1080    // Anchors past the last run.
1081    for anchor in img_iter {
1082        let at = anchor.byte_offset();
1083        push_text(&mut out, cursor, at, None);
1084        out.push(piece(anchor));
1085        cursor = cursor.max(at + sentinel_len(plain_text, at));
1086    }
1087
1088    push_text(&mut out, cursor, text_len, None);
1089
1090    out
1091}
1092
1093/// [`InlinePiece`], but positioned in the document's own **addressable character space**
1094/// rather than as byte offsets local to the block's `plain_text`.
1095///
1096/// `InlinePiece::Text { start, end, .. }` is a byte range good for slicing `plain_text` and
1097/// nothing else: it carries no notion of where in the *document* that range sits. Every
1098/// offset the rest of this crate deals out — `TextDocument::to_addressable_text()`, `find_all`
1099/// match positions, a block's own `document_position` — lives in one **character** space, and
1100/// a caller that reconstructs a document position by summing `InlinePiece` text lengths drifts
1101/// the moment a block holds a multi-byte character before the point in question, or an inline
1102/// image/footnote reference at all — their `U+FFFC` sentinel is three bytes but one char (see
1103/// `sentinel_len` on [`merge_runs_and_anchors`]). That is the same "offset from one space,
1104/// string from another" bug class the public API's `TextDocument::to_addressable_text()`
1105/// exists to close for whole-document offsets (see its doc comment), one level down, inside
1106/// a single block.
1107///
1108/// Built by [`addressable_inline_pieces`] (pure — block-local character space, `base_char_offset`
1109/// left at `0`) or [`crate::format_runs_query::addressable_inline_pieces_for_block`]
1110/// (store-aware — adds the block's own `document_position` so `start`/`end` land in the
1111/// *document's* space). Every downstream consumer that needs a piece boundary in that space —
1112/// a comment's stored quote, a DOCX/ODT comment marker split at an arbitrary character — reads
1113/// `start`/`end` straight off this type instead of re-deriving them by hand.
1114#[derive(Debug, Clone, PartialEq)]
1115pub struct AddressableInlinePiece {
1116    /// `[start, end)` in the addressable character space. `end - start` is always `1` for
1117    /// [`InlineContent::Image`] and [`InlineContent::FootnoteRef`] — the sentinel counts as
1118    /// exactly one character, matching how the document itself counts it — and equals the
1119    /// piece's own text's `chars().count()` for [`InlineContent::Text`].
1120    pub start: u32,
1121    pub end: u32,
1122    pub content: InlineContent,
1123    pub format: CharacterFormat,
1124}
1125
1126/// Build a block's [`AddressableInlinePiece`]s from its `plain_text`, format runs, and
1127/// anchors, offset by `base_char_offset` — the character position this block's own first
1128/// piece should start at.
1129///
1130/// Layered on [`merge_runs_and_anchors`] rather than duplicating its weave: the only thing
1131/// added here is the byte→char conversion (`plain_text[start..end].chars().count()` is the
1132/// only correct way to size a UTF-8 slice in the character space every other offset in this
1133/// crate uses — NOT `end - start`, which is a byte count) and a running `base_char_offset`
1134/// accumulator. Pass `0` to get offsets relative to this block alone, which is what this
1135/// module's own tests do; [`crate::format_runs_query::addressable_inline_pieces_for_block`]
1136/// passes the block's own `document_position` instead, to land in document-wide space.
1137pub fn addressable_inline_pieces(
1138    plain_text: &str,
1139    runs: &[FormatRun],
1140    anchors: &[BlockAnchor<'_>],
1141    base_char_offset: u32,
1142) -> Vec<AddressableInlinePiece> {
1143    let pieces = merge_runs_and_anchors(plain_text, runs, anchors);
1144    let mut out = Vec::with_capacity(pieces.len());
1145    let mut cursor = base_char_offset;
1146    for piece in pieces {
1147        match piece {
1148            InlinePiece::Text { start, end, format } => {
1149                let text = &plain_text[start as usize..end as usize];
1150                let len = text.chars().count() as u32;
1151                out.push(AddressableInlinePiece {
1152                    start: cursor,
1153                    end: cursor + len,
1154                    content: InlineContent::Text(text.to_string()),
1155                    format: format.cloned().unwrap_or_default(),
1156                });
1157                cursor += len;
1158            }
1159            InlinePiece::Image(anchor) => {
1160                out.push(AddressableInlinePiece {
1161                    start: cursor,
1162                    end: cursor + 1,
1163                    content: InlineContent::Image {
1164                        name: anchor.name.clone(),
1165                        alt: anchor.alt.clone(),
1166                        width: anchor.width,
1167                        height: anchor.height,
1168                        quality: anchor.quality,
1169                    },
1170                    format: anchor.format.clone(),
1171                });
1172                cursor += 1;
1173            }
1174            InlinePiece::FootnoteRef(anchor) => {
1175                out.push(AddressableInlinePiece {
1176                    start: cursor,
1177                    end: cursor + 1,
1178                    content: InlineContent::FootnoteRef {
1179                        label: anchor.label.clone(),
1180                    },
1181                    format: anchor.format.clone(),
1182                });
1183                cursor += 1;
1184            }
1185        }
1186    }
1187    out
1188}
1189
1190/// Synthesize a `Vec<InlineSegment>` view of a block from its
1191/// `plain_text`, `format_runs`, and `block_images`. Returns segments
1192/// in document order: a Text segment per format run (with a fallback
1193/// default-format segment for any uncovered bytes), and an Image
1194/// segment per anchor at its byte offset.
1195///
1196/// The canonical reader-side accessor for per-segment data — there is
1197/// no persistent inline-element table; this view is computed fresh
1198/// each call.
1199pub fn inline_segments_view(
1200    plain_text: &str,
1201    runs: &[FormatRun],
1202    images: &[ImageAnchor],
1203    footnote_refs: &[FootnoteRefAnchor],
1204) -> Vec<InlineSegment> {
1205    let bytes = plain_text.as_bytes();
1206    let default_format = CharacterFormat::default();
1207
1208    merge_runs_and_anchors(plain_text, runs, &block_anchors(images, footnote_refs))
1209        .into_iter()
1210        .map(|piece| match piece {
1211            InlinePiece::Text { start, end, format } => {
1212                let slice = &bytes[start as usize..end as usize];
1213                let text = std::str::from_utf8(slice)
1214                    .expect("block plain_text must be valid UTF-8")
1215                    .to_string();
1216                let mut seg = InlineSegment {
1217                    content: InlineContent::Text(text),
1218                    ..Default::default()
1219                };
1220                apply_character_format_to_segment(&mut seg, format.unwrap_or(&default_format));
1221                seg
1222            }
1223            InlinePiece::Image(anchor) => {
1224                let mut seg = InlineSegment {
1225                    content: InlineContent::Image {
1226                        name: anchor.name.clone(),
1227                        alt: anchor.alt.clone(),
1228                        width: anchor.width,
1229                        height: anchor.height,
1230                        quality: anchor.quality,
1231                    },
1232                    ..Default::default()
1233                };
1234                apply_character_format_to_segment(&mut seg, &anchor.format);
1235                seg
1236            }
1237            InlinePiece::FootnoteRef(anchor) => {
1238                let mut seg = InlineSegment {
1239                    content: InlineContent::FootnoteRef {
1240                        label: anchor.label.clone(),
1241                    },
1242                    ..Default::default()
1243                };
1244                apply_character_format_to_segment(&mut seg, &anchor.format);
1245                seg
1246            }
1247        })
1248        .collect()
1249}
1250
1251#[cfg(test)]
1252mod tests {
1253    use super::*;
1254
1255    fn run(s: u32, e: u32, bold: bool) -> FormatRun {
1256        FormatRun {
1257            byte_start: s,
1258            byte_end: e,
1259            format: CharacterFormat {
1260                font_bold: Some(bold),
1261                ..Default::default()
1262            },
1263        }
1264    }
1265
1266    #[test]
1267    fn empty_runs_are_well_formed() {
1268        debug_assert_well_formed(&[], 0);
1269        debug_assert_well_formed(&[], 100);
1270    }
1271
1272    // ── merge_runs_and_anchors ────────────────────────────────────────────
1273
1274    fn anchor(at: u32, name: &str) -> ImageAnchor {
1275        ImageAnchor {
1276            byte_offset: at,
1277            name: name.into(),
1278            alt: String::new(),
1279            width: 10,
1280            height: 10,
1281            quality: 100,
1282            format: CharacterFormat::default(),
1283        }
1284    }
1285
1286    fn fn_anchor(at: u32, label: &str) -> FootnoteRefAnchor {
1287        FootnoteRefAnchor {
1288            byte_offset: at,
1289            label: label.into(),
1290            format: CharacterFormat::default(),
1291        }
1292    }
1293
1294    /// Render a merge as a compact string so ordering assertions read clearly:
1295    /// `"hello"` for unformatted text, `"*bold*"` for formatted, `[name]` for
1296    /// an image, `^label^` for a footnote reference.
1297    fn shape(text: &str, runs: &[FormatRun], images: &[ImageAnchor]) -> String {
1298        shape_with(text, runs, images, &[])
1299    }
1300
1301    fn shape_with(
1302        text: &str,
1303        runs: &[FormatRun],
1304        images: &[ImageAnchor],
1305        notes: &[FootnoteRefAnchor],
1306    ) -> String {
1307        merge_runs_and_anchors(text, runs, &block_anchors(images, notes))
1308            .into_iter()
1309            .map(|p| match p {
1310                InlinePiece::Text { start, end, format } => {
1311                    let s = &text[start as usize..end as usize];
1312                    if format.is_some() {
1313                        format!("*{s}*")
1314                    } else {
1315                        s.to_string()
1316                    }
1317                }
1318                InlinePiece::Image(a) => format!("[{}]", a.name),
1319                InlinePiece::FootnoteRef(a) => format!("^{}^", a.label),
1320            })
1321            .collect::<Vec<_>>()
1322            .join("|")
1323    }
1324
1325    /// A footnote reference weaves exactly as an image does — the whole reason
1326    /// the two share one function instead of getting a second copy of it.
1327    #[test]
1328    fn a_footnote_reference_inside_a_run_splits_it() {
1329        let text = "abcdef";
1330        let runs = [run(0, 6, true)];
1331        assert_eq!(
1332            shape_with(text, &runs, &[], &[fn_anchor(3, "n1")]),
1333            "*abc*|^n1^|*def*"
1334        );
1335    }
1336
1337    /// Images and references are stored in separate lists but occupy one
1338    /// stream. Walking the two lists in sequence rather than merging them by
1339    /// offset would emit every image before every note regardless of where they
1340    /// actually sit — the ordering bug this weave exists to prevent, in a new
1341    /// disguise.
1342    #[test]
1343    fn images_and_references_interleave_by_position() {
1344        let text = "abcdefgh";
1345        assert_eq!(
1346            shape_with(
1347                text,
1348                &[],
1349                &[anchor(6, "img")],
1350                &[fn_anchor(2, "early"), fn_anchor(7, "late")]
1351            ),
1352            "ab|^early^|cdef|[img]|g|^late^|h"
1353        );
1354    }
1355
1356    /// **The regression.** An image anchored inside a formatted run used to be
1357    /// skipped by the run loop (which only tested `offset < run.byte_start`)
1358    /// and swept up by the trailing loop, landing after every run in the block.
1359    /// Put a picture mid-sentence in bold text and every exporter moved it to
1360    /// the end of the paragraph.
1361    #[test]
1362    fn an_image_inside_a_run_splits_it_instead_of_jumping_to_the_end() {
1363        let text = "abcdef";
1364        let runs = [run(0, 6, true)];
1365        let images = [anchor(3, "img")];
1366        assert_eq!(shape(text, &runs, &images), "*abc*|[img]|*def*");
1367    }
1368
1369    #[test]
1370    fn an_image_on_a_run_start_boundary_stays_in_place() {
1371        let text = "abcdef";
1372        let runs = [run(3, 6, true)];
1373        assert_eq!(shape(text, &runs, &[anchor(3, "i")]), "abc|[i]|*def*");
1374    }
1375
1376    /// The `<=` in the in-run loop: an image exactly on a run's end boundary is
1377    /// consumed with that run rather than deferred to the trailing sweep.
1378    #[test]
1379    fn an_image_on_a_run_end_boundary_stays_in_place() {
1380        let text = "abcdef";
1381        let runs = [run(0, 3, true)];
1382        assert_eq!(shape(text, &runs, &[anchor(3, "i")]), "*abc*|[i]|def");
1383    }
1384
1385    #[test]
1386    fn an_image_between_two_runs_lands_between_them() {
1387        let text = "abcdef";
1388        let runs = [run(0, 3, true), run(3, 6, false)];
1389        let out = shape(text, &runs, &[anchor(3, "i")]);
1390        assert_eq!(out, "*abc*|[i]|*def*");
1391    }
1392
1393    #[test]
1394    fn several_images_inside_one_run_keep_their_order() {
1395        let text = "abcdefgh";
1396        let runs = [run(0, 8, true)];
1397        let images = [anchor(2, "a"), anchor(5, "b")];
1398        assert_eq!(shape(text, &runs, &images), "*ab*|[a]|*cde*|[b]|*fgh*");
1399    }
1400
1401    #[test]
1402    fn two_images_at_the_same_offset_both_survive_in_order() {
1403        let text = "abcd";
1404        let runs = [run(0, 4, true)];
1405        let images = [anchor(2, "a"), anchor(2, "b")];
1406        assert_eq!(shape(text, &runs, &images), "*ab*|[a]|[b]|*cd*");
1407    }
1408
1409    #[test]
1410    fn images_with_no_runs_at_all_are_ordered_with_their_gaps() {
1411        let text = "abcdef";
1412        let images = [anchor(0, "a"), anchor(3, "b"), anchor(6, "c")];
1413        assert_eq!(shape(text, &[], &images), "[a]|abc|[b]|def|[c]");
1414    }
1415
1416    #[test]
1417    fn text_uncovered_by_any_run_stays_unformatted() {
1418        let text = "abcdef";
1419        let runs = [run(2, 4, true)];
1420        assert_eq!(shape(text, &runs, &[]), "ab|*cd*|ef");
1421    }
1422
1423    #[test]
1424    fn a_block_with_neither_runs_nor_images_is_one_plain_piece() {
1425        assert_eq!(shape("abc", &[], &[]), "abc");
1426        assert_eq!(shape("", &[], &[]), "");
1427    }
1428
1429    /// Whatever the arrangement, the merge must reproduce the block's bytes
1430    /// exactly once, in order — never dropping or duplicating a slice. The
1431    /// old implementation could regress its cursor and re-emit text twice.
1432    #[test]
1433    fn every_byte_is_emitted_exactly_once_and_in_order() {
1434        let text = "abcdefghij";
1435        let arrangements: [(&[FormatRun], &[ImageAnchor]); 6] = [
1436            (&[], &[]),
1437            (&[run(0, 10, true)], &[anchor(5, "m")]),
1438            (&[run(2, 5, true), run(5, 8, false)], &[anchor(5, "m")]),
1439            (&[run(2, 5, true)], &[anchor(0, "a"), anchor(10, "z")]),
1440            (&[run(0, 3, true), run(7, 10, true)], &[anchor(3, "m")]),
1441            (
1442                &[run(1, 4, true), run(4, 9, false)],
1443                &[anchor(1, "a"), anchor(4, "b"), anchor(9, "c")],
1444            ),
1445        ];
1446        for (i, (runs, images)) in arrangements.iter().enumerate() {
1447            let pieces = merge_runs_and_anchors(text, runs, &block_anchors(images, &[]));
1448            let mut cursor = 0u32;
1449            let mut rebuilt = String::new();
1450            for piece in &pieces {
1451                if let InlinePiece::Text { start, end, .. } = piece {
1452                    assert_eq!(*start, cursor, "arrangement {i}: gap or overlap");
1453                    assert!(start < end, "arrangement {i}: empty piece emitted");
1454                    rebuilt.push_str(&text[*start as usize..*end as usize]);
1455                    cursor = *end;
1456                }
1457            }
1458            assert_eq!(cursor, text.len() as u32, "arrangement {i}: truncated");
1459            assert_eq!(rebuilt, text, "arrangement {i}");
1460            let img_count = pieces
1461                .iter()
1462                .filter(|p| matches!(p, InlinePiece::Image(_)))
1463                .count();
1464            assert_eq!(img_count, images.len(), "arrangement {i}: lost an image");
1465        }
1466    }
1467
1468    /// `inline_segments_view` is built on the merge, so it inherits the fix.
1469    #[test]
1470    fn inline_segments_view_places_a_mid_run_image_correctly() {
1471        let segs = inline_segments_view("abcdef", &[run(0, 6, true)], &[anchor(3, "img")], &[]);
1472        assert_eq!(segs.len(), 3);
1473        assert!(matches!(&segs[0].content, InlineContent::Text(t) if t == "abc"));
1474        assert!(matches!(&segs[1].content, InlineContent::Image { name, .. } if name == "img"));
1475        assert!(matches!(&segs[2].content, InlineContent::Text(t) if t == "def"));
1476        // The split keeps the run's formatting on both sides of the image.
1477        assert_eq!(segs[0].fmt_font_bold, Some(true));
1478        assert_eq!(segs[2].fmt_font_bold, Some(true));
1479    }
1480
1481    #[test]
1482    fn inline_segments_view_carries_alt_text_through() {
1483        let mut a = anchor(1, "img");
1484        a.alt = "a black cat".into();
1485        let segs = inline_segments_view("ab", &[], &[a], &[]);
1486        let alt = segs.iter().find_map(|s| match &s.content {
1487            InlineContent::Image { alt, .. } => Some(alt.clone()),
1488            _ => None,
1489        });
1490        assert_eq!(alt.as_deref(), Some("a black cat"));
1491    }
1492
1493    // ── addressable_inline_pieces ───────────────────────────────────────
1494
1495    /// Render an `addressable_inline_pieces` result as `(start, end, label)` triples, so a
1496    /// failing assertion shows the whole span list at once instead of one field at a time.
1497    fn pieces_shape(out: &[AddressableInlinePiece]) -> Vec<(u32, u32, String)> {
1498        out.iter()
1499            .map(|p| {
1500                let label = match &p.content {
1501                    InlineContent::Text(t) => t.clone(),
1502                    InlineContent::Image { name, .. } => format!("[{name}]"),
1503                    InlineContent::FootnoteRef { label } => format!("^{label}^"),
1504                    InlineContent::Empty => String::new(),
1505                };
1506                (p.start, p.end, label)
1507            })
1508            .collect()
1509    }
1510
1511    #[test]
1512    fn a_lone_text_piece_spans_its_char_length() {
1513        let out = addressable_inline_pieces("hello", &[], &[], 0);
1514        assert_eq!(pieces_shape(&out), vec![(0, 5, "hello".to_string())]);
1515    }
1516
1517    #[test]
1518    fn base_char_offset_shifts_every_piece_by_the_same_amount() {
1519        let out = addressable_inline_pieces("hello", &[], &[], 100);
1520        assert_eq!(pieces_shape(&out), vec![(100, 105, "hello".to_string())]);
1521    }
1522
1523    /// The regression this type exists to prevent: a multi-byte character before an anchor
1524    /// must not leak its extra BYTE into the anchor's CHAR position. "café" is 5 bytes (é is
1525    /// 2 bytes in UTF-8) but 4 chars — code that summed byte lengths instead of counting
1526    /// chars would place the image at char 5, one past where it actually sits, and every
1527    /// offset after it would carry the same one-character drift.
1528    #[test]
1529    fn a_multibyte_character_before_an_image_does_not_leak_its_extra_byte_into_the_char_position() {
1530        let text = "café";
1531        assert_eq!(text.len(), 5, "test assumes café is 5 bytes");
1532        assert_eq!(text.chars().count(), 4, "test assumes café is 4 chars");
1533        let images = [anchor(5, "pic")]; // byte_offset 5 == right after "café"'s 5 bytes
1534        let anchors = block_anchors(&images, &[]);
1535        let out = addressable_inline_pieces(text, &[], &anchors, 0);
1536        assert_eq!(
1537            pieces_shape(&out),
1538            vec![(0, 4, "café".to_string()), (4, 5, "[pic]".to_string())]
1539        );
1540    }
1541
1542    /// An image sitting inside a formatted run gets its own one-char span, and the text
1543    /// before/after it still lands at the right char offsets — the boundary this milestone's
1544    /// comment-export use case cares about most: "a comment boundary landing immediately
1545    /// after an inline image."
1546    #[test]
1547    fn an_image_mid_run_gets_a_one_char_span_and_the_trailing_text_resumes_right_after_it() {
1548        let text = "abcdef";
1549        let runs = [run(0, 6, true)];
1550        let images = [anchor(3, "img")];
1551        let anchors = block_anchors(&images, &[]);
1552        let out = addressable_inline_pieces(text, &runs, &anchors, 0);
1553        assert_eq!(
1554            pieces_shape(&out),
1555            vec![
1556                (0, 3, "abc".to_string()),
1557                (3, 4, "[img]".to_string()),
1558                (4, 7, "def".to_string()),
1559            ]
1560        );
1561    }
1562
1563    /// A footnote reference gets a one-char span exactly like an image — the boundary
1564    /// case "a comment boundary landing immediately after a footnote reference."
1565    #[test]
1566    fn a_footnote_reference_gets_a_one_char_span_too() {
1567        let text = "abcdef";
1568        let notes = [fn_anchor(3, "n1")];
1569        let anchors = block_anchors(&[], &notes);
1570        let out = addressable_inline_pieces(text, &[], &anchors, 0);
1571        assert_eq!(
1572            pieces_shape(&out),
1573            vec![
1574                (0, 3, "abc".to_string()),
1575                (3, 4, "^n1^".to_string()),
1576                (4, 7, "def".to_string()),
1577            ]
1578        );
1579    }
1580
1581    /// Whatever the arrangement of runs and anchors, the pieces must tile the block's char
1582    /// space with no gap, no overlap, and no dropped anchor — the char-space analogue of
1583    /// `every_byte_is_emitted_exactly_once_and_in_order` above.
1584    #[test]
1585    fn pieces_are_contiguous_in_char_space_with_no_gaps_or_overlaps() {
1586        let text = "abcdefghij";
1587        let arrangements: [(&[FormatRun], &[ImageAnchor]); 4] = [
1588            (&[], &[]),
1589            (&[run(0, 10, true)], &[anchor(5, "m")]),
1590            (&[run(2, 5, true), run(5, 8, false)], &[anchor(5, "m")]),
1591            (&[run(1, 4, true), run(4, 9, false)], &[anchor(9, "c")]),
1592        ];
1593        for (i, (runs, images)) in arrangements.iter().enumerate() {
1594            let anchors = block_anchors(images, &[]);
1595            let out = addressable_inline_pieces(text, runs, &anchors, 7);
1596            let mut cursor = 7u32;
1597            for piece in &out {
1598                assert_eq!(piece.start, cursor, "arrangement {i}: gap or overlap");
1599                assert!(piece.start < piece.end, "arrangement {i}: empty piece");
1600                cursor = piece.end;
1601            }
1602            // Each anchor is a logical character with no bytes of its own in `text`, so
1603            // it adds one char beyond `text`'s own count — same accounting `block_char_length`
1604            // uses for a real (sentinel-bearing) block, just without a sentinel to count here.
1605            assert_eq!(
1606                cursor,
1607                7 + text.chars().count() as u32 + images.len() as u32,
1608                "arrangement {i}: did not tile the whole block"
1609            );
1610        }
1611    }
1612
1613    #[test]
1614    fn coalesce_merges_adjacent_equal_runs() {
1615        let mut rs = vec![run(0, 5, true), run(5, 10, true), run(10, 15, false)];
1616        coalesce_in_place(&mut rs);
1617        assert_eq!(rs.len(), 2);
1618        assert_eq!(rs[0].byte_end, 10);
1619    }
1620
1621    #[test]
1622    fn coalesce_leaves_disjoint_runs_alone() {
1623        let mut rs = vec![run(0, 5, true), run(7, 10, true)];
1624        coalesce_in_place(&mut rs);
1625        assert_eq!(rs.len(), 2);
1626    }
1627
1628    #[test]
1629    fn splice_range_clips_straddling_runs() {
1630        let mut rs = vec![run(0, 20, true)];
1631        splice_range(&mut rs, 5..15, vec![run(5, 15, false)]);
1632        assert_eq!(rs.len(), 3);
1633        assert_eq!(rs[0].byte_end, 5);
1634        assert_eq!(rs[1].format.font_bold, Some(false));
1635        assert_eq!(rs[2].byte_start, 15);
1636    }
1637
1638    #[test]
1639    fn splice_range_empty_replacement_removes_inner_runs() {
1640        let mut rs = vec![run(0, 5, true), run(5, 10, false), run(10, 15, true)];
1641        splice_range(&mut rs, 5..10, vec![]);
1642        // 0..5 bold, then 10..15 bold — after coalesce these are NOT adjacent
1643        // (there's a gap from 5..10 in the run table, meaning "no format").
1644        assert_eq!(rs.len(), 2);
1645        assert_eq!(rs[0].byte_end, 5);
1646        assert_eq!(rs[1].byte_start, 10);
1647    }
1648
1649    #[test]
1650    fn shift_after_moves_downstream() {
1651        let mut rs = vec![run(0, 5, true), run(10, 15, false)];
1652        shift_after(&mut rs, 5, 3);
1653        assert_eq!(rs[0].byte_start, 0); // unchanged
1654        assert_eq!(rs[1].byte_start, 13);
1655        assert_eq!(rs[1].byte_end, 18);
1656    }
1657}
1658
1659/// The adversarial corpus for [`shift_runs_for_replace`].
1660///
1661/// Replace is the one edit that rewrites a writer's prose *in bulk*, so its formatting
1662/// behaviour has to be pinned rather than inherited by accident. Before this module
1663/// there were 11 replace tests in the repo and **not one** of them mentioned
1664/// `FormatRun`: the behaviour was emergent, undecided, and untested.
1665#[cfg(test)]
1666mod replace_policy_tests {
1667    use super::*;
1668
1669    fn fmt(tag: &str) -> CharacterFormat {
1670        CharacterFormat {
1671            font_bold: Some(tag == "B"),
1672            font_italic: Some(tag == "I"),
1673            ..Default::default()
1674        }
1675    }
1676    fn r(start: u32, end: u32, tag: &str) -> FormatRun {
1677        FormatRun {
1678            byte_start: start,
1679            byte_end: end,
1680            format: fmt(tag),
1681        }
1682    }
1683    /// Compact "0..5=B 8..12=I" rendering, so a failure shows the whole run list.
1684    fn show(runs: &[FormatRun]) -> String {
1685        if runs.is_empty() {
1686            return "[]".to_string();
1687        }
1688        runs.iter()
1689            .map(|x| {
1690                let tag = if x.format.font_bold == Some(true) {
1691                    "B"
1692                } else if x.format.font_italic == Some(true) {
1693                    "I"
1694                } else {
1695                    "p"
1696                };
1697                format!("{}..{}={tag}", x.byte_start, x.byte_end)
1698            })
1699            .collect::<Vec<_>>()
1700            .join(" ")
1701    }
1702    fn replace(
1703        runs: &[FormatRun],
1704        start: u32,
1705        end: u32,
1706        n: u32,
1707        policy: ReplaceFormatPolicy,
1708    ) -> Vec<FormatRun> {
1709        let mut runs = runs.to_vec();
1710        shift_runs_for_replace(&mut runs, start, end, n, policy).expect("valid replace");
1711        runs
1712    }
1713
1714    /// **The spec-conformance test.** `InheritPreceding` must be byte-for-byte what
1715    /// today's `shift_runs_for_delete` + `shift_runs_for_insert` produces — anything
1716    /// else silently rewrites the formatting of every replace that ever shipped.
1717    ///
1718    /// Differential, not hand-transcribed: it runs the historical primitives directly
1719    /// and compares, so it stays honest even if their behaviour is ever changed.
1720    #[test]
1721    fn inherit_preceding_matches_the_historical_delete_then_insert() {
1722        let corpus: Vec<(&str, Vec<FormatRun>, u32, u32, u32)> = vec![
1723            ("run ends exactly at start", vec![r(0, 5, "B")], 5, 10, 3),
1724            ("run begins exactly at start", vec![r(5, 8, "B")], 5, 10, 3),
1725            (
1726                "run begins at start, outlives end",
1727                vec![r(5, 20, "B")],
1728                5,
1729                10,
1730                3,
1731            ),
1732            (
1733                "run straddles the whole range",
1734                vec![r(0, 20, "B")],
1735                5,
1736                10,
1737                3,
1738            ),
1739            ("no run touches the start", vec![r(12, 20, "B")], 5, 10, 3),
1740            ("bold tail inside the range", vec![r(9, 13, "B")], 5, 13, 4),
1741            ("pure delete", vec![r(0, 20, "B")], 5, 10, 0),
1742            ("pure insert", vec![r(0, 20, "B")], 5, 5, 3),
1743            (
1744                "same format either side coalesces",
1745                vec![r(0, 5, "B"), r(10, 15, "B")],
1746                5,
1747                10,
1748                3,
1749            ),
1750            (
1751                "different formats either side",
1752                vec![r(0, 5, "B"), r(10, 15, "I")],
1753                5,
1754                10,
1755                3,
1756            ),
1757            ("empty run list", vec![], 5, 10, 3),
1758            ("the only run is consumed", vec![r(5, 10, "B")], 5, 10, 3),
1759            (
1760                "replacement longer than the range",
1761                vec![r(0, 5, "B")],
1762                5,
1763                10,
1764                20,
1765            ),
1766            (
1767                "three runs straddled",
1768                vec![r(0, 3, "B"), r(3, 6, "I"), r(6, 9, "B")],
1769                2,
1770                7,
1771                4,
1772            ),
1773            (
1774                "gap between two same-format runs is deleted",
1775                vec![r(0, 5, "B"), r(8, 13, "B")],
1776                5,
1777                8,
1778                0,
1779            ),
1780        ];
1781
1782        for (name, runs, start, end, n) in corpus {
1783            // The historical composition, run for real.
1784            let mut expected = runs.clone();
1785            shift_runs_for_delete(&mut expected, start, end);
1786            shift_runs_for_insert(&mut expected, start, n);
1787
1788            let got = replace(&runs, start, end, n, ReplaceFormatPolicy::InheritPreceding);
1789
1790            assert_eq!(
1791                show(&got),
1792                show(&expected),
1793                "InheritPreceding diverged from delete+insert for {name:?} \
1794                 (replace {start}..{end}, n={n})\n  before:   {}\n  historical: {}\n  got:        {}",
1795                show(&runs),
1796                show(&expected),
1797                show(&got),
1798            );
1799        }
1800    }
1801
1802    /// The motivating data loss: renaming a character whose name reads `Auré**lien**`.
1803    /// The default drops the bold — that is what shipped, and it is now visible and
1804    /// chosen rather than emergent. Every other policy is a way to not lose it.
1805    #[test]
1806    fn the_four_policies_diverge_on_a_partly_bold_name() {
1807        // "Auré" plain (bytes 0..5, é is two bytes), "lien" bold (5..9).
1808        let runs = vec![r(5, 9, "B")];
1809        let (start, end, n) = (0, 9, 9); // rename the whole name, same length
1810
1811        use ReplaceFormatPolicy::*;
1812        assert_eq!(
1813            show(&replace(&runs, start, end, n, InheritPreceding)),
1814            "[]",
1815            "the historical default destroys the bold — pinned, not endorsed"
1816        );
1817        assert_eq!(
1818            show(&replace(&runs, start, end, n, PreserveNothing)),
1819            "[]",
1820            "explicitly unformatted"
1821        );
1822        assert_eq!(
1823            show(&replace(&runs, start, end, n, PreserveIfFullyCovered)),
1824            "[]",
1825            "no SINGLE run covers 0..9 — it must fall back to inheritance, not guess"
1826        );
1827        // Bold covers 4 of the 9 bytes, plain covers 5 → plain dominates.
1828        assert_eq!(
1829            show(&replace(&runs, start, end, n, KeepDominantRun)),
1830            "[]",
1831            "plain covers more of the name than the bold does"
1832        );
1833
1834        // …but when the bold covers MOST of the name, KeepDominantRun keeps it.
1835        let mostly_bold = vec![r(1, 9, "B")];
1836        assert_eq!(
1837            show(&replace(&mostly_bold, 0, 9, 9, KeepDominantRun)),
1838            "0..9=B",
1839            "bold covers 8 of 9 bytes — the rename must keep it"
1840        );
1841    }
1842
1843    /// "Fully covered" means ONE run covers the range — not "the runs jointly span it".
1844    /// A gapless Italic+Bold union spanning the range exactly must NOT be treated as
1845    /// covered, or the replacement silently inherits whichever run was looked at first.
1846    #[test]
1847    fn fully_covered_means_a_single_run_not_a_gapless_union() {
1848        let two = vec![r(0, 3, "I"), r(3, 10, "B")];
1849        assert_eq!(
1850            show(&replace(
1851                &two,
1852                0,
1853                10,
1854                4,
1855                ReplaceFormatPolicy::PreserveIfFullyCovered
1856            )),
1857            "[]",
1858            "two different-format runs jointly spanning the range are not 'covered'; \
1859             with no run preceding the start, the fallback is unformatted"
1860        );
1861
1862        // One run that really does cover it, and begins exactly at the start — the case
1863        // InheritPreceding cannot see (a run at `start` never inherits).
1864        let one = vec![r(5, 20, "B")];
1865        assert_eq!(
1866            show(&replace(
1867                &one,
1868                5,
1869                10,
1870                3,
1871                ReplaceFormatPolicy::PreserveIfFullyCovered
1872            )),
1873            "5..18=B",
1874            "a single covering run keeps its format across the rename"
1875        );
1876        assert_eq!(
1877            show(&replace(
1878                &one,
1879                5,
1880                10,
1881                3,
1882                ReplaceFormatPolicy::InheritPreceding
1883            )),
1884            "8..18=B",
1885            "…which the default would have lost: the replacement lands unformatted"
1886        );
1887    }
1888
1889    /// A run that merely *touches* the range must not leak its format to the whole
1890    /// replacement.
1891    #[test]
1892    fn a_partially_overlapping_run_does_not_count_as_covering() {
1893        let runs = vec![r(0, 8, "B")]; // covers only 5..8 of the range 5..12
1894        assert_eq!(
1895            show(&replace(
1896                &runs,
1897                5,
1898                12,
1899                4,
1900                ReplaceFormatPolicy::PreserveIfFullyCovered
1901            )),
1902            "0..9=B",
1903            "not covered → falls back to inheritance, which extends the preceding bold; \
1904             it must NOT format the whole replacement as though bold had covered it"
1905        );
1906    }
1907
1908    /// Ties between two runs resolve to the EARLIEST, deterministically.
1909    ///
1910    /// `Iterator::max_by_key` returns the LAST maximum, so the obvious one-liner would
1911    /// have silently picked the other run here.
1912    #[test]
1913    fn a_dominance_tie_between_two_runs_goes_to_the_earlier() {
1914        let runs = vec![r(0, 3, "B"), r(3, 6, "I")]; // 3 bytes each
1915        assert_eq!(
1916            show(&replace(
1917                &runs,
1918                0,
1919                6,
1920                4,
1921                ReplaceFormatPolicy::KeepDominantRun
1922            )),
1923            "0..4=B",
1924            "a true tie must resolve to the earlier run, not to whichever the iterator \
1925             happened to visit last"
1926        );
1927    }
1928
1929    /// A tie between a run and the unformatted gap goes to the run: losing formatting is
1930    /// the destructive outcome, so it needs a strict majority of *plain* to win.
1931    #[test]
1932    fn a_dominance_tie_against_plain_text_keeps_the_formatting() {
1933        let runs = vec![r(4, 8, "B")]; // 4 bold bytes, 4 plain bytes in 0..8
1934        assert_eq!(
1935            show(&replace(
1936                &runs,
1937                0,
1938                8,
1939                5,
1940                ReplaceFormatPolicy::KeepDominantRun
1941            )),
1942            "0..5=B",
1943            "an even split must keep the formatting rather than silently drop it"
1944        );
1945    }
1946
1947    /// An empty range replaces nothing, so it is an insertion — and every policy that
1948    /// reasons about "what was covered" must defer to the insert convention instead of
1949    /// stripping the format the typed text would have inherited.
1950    #[test]
1951    fn an_empty_range_is_an_insert_and_no_coverage_policy_overrides_it() {
1952        let runs = vec![r(0, 5, "B"), r(5, 10, "I")];
1953        use ReplaceFormatPolicy::*;
1954        for policy in [InheritPreceding, PreserveIfFullyCovered, KeepDominantRun] {
1955            assert_eq!(
1956                show(&replace(&runs, 5, 5, 2, policy)),
1957                "0..7=B 7..12=I",
1958                "{policy:?}: typing at a boundary must inherit the run to the LEFT (Qt \
1959                 convention) — an empty range destroyed no formatting, so there is \
1960                 nothing for a coverage policy to override"
1961            );
1962        }
1963        // The one policy that is an explicit request, not an inference, still applies.
1964        assert_eq!(
1965            show(&replace(&runs, 5, 5, 2, PreserveNothing)),
1966            "0..5=B 7..12=I",
1967            "PreserveNothing asks for unformatted text, and means it even on an insert"
1968        );
1969    }
1970
1971    /// Nothing to do must mean nothing done — no fabricated runs, no lost ones.
1972    #[test]
1973    fn a_zero_width_zero_length_replace_is_the_identity() {
1974        let runs = vec![r(0, 5, "B"), r(7, 12, "I")];
1975        for policy in [
1976            ReplaceFormatPolicy::InheritPreceding,
1977            ReplaceFormatPolicy::PreserveIfFullyCovered,
1978            ReplaceFormatPolicy::KeepDominantRun,
1979            ReplaceFormatPolicy::PreserveNothing,
1980        ] {
1981            assert_eq!(
1982                show(&replace(&runs, 6, 6, 0, policy)),
1983                "0..5=B 7..12=I",
1984                "{policy:?} changed a no-op edit"
1985            );
1986        }
1987    }
1988
1989    /// `PreserveNothing` must leave the region genuinely *unformatted* — not covered by
1990    /// a fabricated run carrying `CharacterFormat::default()`, which is a different
1991    /// thing and would defeat coalescing forever after.
1992    #[test]
1993    fn preserve_nothing_fabricates_no_default_run() {
1994        let runs = vec![r(0, 5, "B")];
1995        let got = replace(&runs, 7, 9, 2, ReplaceFormatPolicy::PreserveNothing);
1996        assert_eq!(show(&got), "0..5=B", "no run may be invented for the gap");
1997        assert!(
1998            got.iter().all(|x| x.byte_start < 7 || x.byte_end > 9),
1999            "the replaced span must carry no run at all"
2000        );
2001    }
2002
2003    /// Offsets are BYTES, not chars. A 4-byte emoji replaced by 2 ASCII bytes must
2004    /// shift downstream runs by -2, not by -1 (chars) or 0.
2005    #[test]
2006    fn offsets_are_bytes_not_characters() {
2007        // "🎉" (4 bytes, bold) + " " + "abcd" (italic).
2008        let runs = vec![r(0, 4, "B"), r(5, 9, "I")];
2009        let got = replace(&runs, 0, 4, 2, ReplaceFormatPolicy::KeepDominantRun);
2010        assert_eq!(
2011            show(&got),
2012            "0..2=B 3..7=I",
2013            "the trailing italic must shift back by the BYTE delta (4 -> 2 = -2)"
2014        );
2015    }
2016
2017    /// Every policy must leave the run list well-formed — sorted, non-overlapping,
2018    /// coalesced, inside the block. This is the invariant a release build no longer
2019    /// merely asserts.
2020    #[test]
2021    fn every_policy_leaves_the_runs_well_formed() {
2022        let setups: Vec<(Vec<FormatRun>, u32, u32, u32, usize)> = vec![
2023            (vec![r(0, 3, "B"), r(3, 6, "I"), r(6, 9, "B")], 2, 7, 4, 8),
2024            (vec![r(0, 5, "B"), r(8, 13, "B")], 5, 8, 0, 10),
2025            (vec![r(0, 5, "B"), r(7, 12, "B")], 8, 10, 2, 12),
2026            (vec![r(3, 7, "B")], 3, 7, 0, 6),
2027            (vec![], 2, 6, 3, 9),
2028        ];
2029        for (runs, start, end, n, text_len) in setups {
2030            for policy in [
2031                ReplaceFormatPolicy::InheritPreceding,
2032                ReplaceFormatPolicy::PreserveIfFullyCovered,
2033                ReplaceFormatPolicy::KeepDominantRun,
2034                ReplaceFormatPolicy::PreserveNothing,
2035            ] {
2036                let got = replace(&runs, start, end, n, policy);
2037                check_well_formed(&got, text_len).unwrap_or_else(|e| {
2038                    panic!(
2039                        "{policy:?} produced malformed runs from {} (replace {start}..{end}, \
2040                         n={n}): {} — {e}",
2041                        show(&runs),
2042                        show(&got)
2043                    )
2044                });
2045            }
2046        }
2047    }
2048
2049    /// Two same-format runs separated by an untouched gap must stay separate — coalescing
2050    /// is for *adjacent* runs, and over-eager merging would swallow the plain text between
2051    /// them.
2052    #[test]
2053    fn an_untouched_gap_between_equal_runs_survives() {
2054        let runs = vec![r(0, 5, "B"), r(7, 12, "B")];
2055        assert_eq!(
2056            show(&replace(
2057                &runs,
2058                8,
2059                10,
2060                2,
2061                ReplaceFormatPolicy::InheritPreceding
2062            )),
2063            "0..5=B 7..12=B",
2064            "the plain gap at 5..7 must not be swallowed"
2065        );
2066    }
2067
2068    /// An inverted range is refused, not asserted away.
2069    #[test]
2070    fn an_inverted_range_is_an_error_not_a_panic() {
2071        let mut runs = vec![r(0, 5, "B")];
2072        let err =
2073            shift_runs_for_replace(&mut runs, 10, 5, 3, ReplaceFormatPolicy::InheritPreceding)
2074                .expect_err("an inverted range must be rejected");
2075        assert!(matches!(
2076            err,
2077            FormatRunError::ReversedRange { start: 10, end: 5 }
2078        ));
2079        assert_eq!(show(&runs), "0..5=B", "a refused edit must change nothing");
2080    }
2081}
2082
2083/// The runtime guards that replaced the release-mode-invisible `debug_assert!`s.
2084#[cfg(test)]
2085mod invariant_check_tests {
2086    use super::*;
2087
2088    fn run(start: u32, end: u32, bold: bool) -> FormatRun {
2089        FormatRun {
2090            byte_start: start,
2091            byte_end: end,
2092            format: CharacterFormat {
2093                font_bold: Some(bold),
2094                ..Default::default()
2095            },
2096        }
2097    }
2098
2099    /// The whole point: in a **release** build `debug_assert!` is gone, so a violating
2100    /// splice used to sail straight through and build a malformed run list. Now it is
2101    /// reported, and the caller's runs are left exactly as they were.
2102    #[test]
2103    fn a_replacement_outside_the_range_is_rejected_without_mutating() {
2104        let mut runs = vec![run(0, 20, true)];
2105        let before = runs.clone();
2106
2107        let err = try_splice_range(&mut runs, 5..10, vec![run(5, 15, false)])
2108            .expect_err("a replacement run reaching past range.end must be rejected");
2109
2110        assert!(matches!(
2111            err,
2112            FormatRunError::ReplacementOutsideRange {
2113                run_end: 15,
2114                range_end: 10,
2115                ..
2116            }
2117        ));
2118        assert_eq!(
2119            runs, before,
2120            "a rejected splice must not half-apply — validation happens before mutation"
2121        );
2122    }
2123
2124    #[test]
2125    // The inverted range is the whole point of the test — it is what a caller must not
2126    // be able to sneak past a release build.
2127    #[allow(clippy::reversed_empty_ranges)]
2128    fn a_reversed_range_is_rejected() {
2129        let mut runs = vec![run(0, 20, true)];
2130        assert!(matches!(
2131            try_splice_range(&mut runs, 10..5, vec![]),
2132            Err(FormatRunError::ReversedRange { start: 10, end: 5 })
2133        ));
2134    }
2135
2136    #[test]
2137    fn a_legal_splice_still_works_through_the_checked_path() {
2138        let mut runs = vec![run(0, 20, true)];
2139        try_splice_range(&mut runs, 5..15, vec![run(5, 15, false)]).expect("legal");
2140        assert_eq!(runs.len(), 3);
2141        assert_eq!(runs[1].format.font_bold, Some(false));
2142    }
2143
2144    #[test]
2145    fn check_well_formed_catches_what_debug_assert_used_to() {
2146        assert!(check_well_formed(&[], 0).is_ok());
2147        assert!(check_well_formed(&[run(0, 5, true)], 5).is_ok());
2148
2149        assert!(matches!(
2150            check_well_formed(&[run(5, 5, true)], 10),
2151            Err(FormatRunError::EmptyRun { .. })
2152        ));
2153        assert!(matches!(
2154            check_well_formed(&[run(0, 8, true), run(5, 10, false)], 10),
2155            Err(FormatRunError::RunsOverlap { .. })
2156        ));
2157        assert!(matches!(
2158            check_well_formed(&[run(0, 5, true), run(5, 10, true)], 10),
2159            Err(FormatRunError::RunsNotCoalesced { .. })
2160        ));
2161        assert!(matches!(
2162            check_well_formed(&[run(0, 20, true)], 10),
2163            Err(FormatRunError::RunPastEndOfBlock { text_len: 10, .. })
2164        ));
2165    }
2166}