Skip to main content

fleuron/
content.rs

1//! The content tree: semantic input.
2//!
3//! The markdown frontend produces this; the element vocabulary is
4//! bounded by what a book needs — book/section, heading, paragraph,
5//! blockquote, thematic break, image, list, table,
6//! emphasis/strong/code, link, hard break.
7//!
8//! This module is the **input contract**: everything downstream (style,
9//! box construction, layout) consumes these types, and nothing widens
10//! the vocabulary without a fixture and a test. It is a Rust type, and
11//! that is the seam a frontend of its own builds against: a docx or CMS
12//! reader constructs a `Book` directly, with the compiler checking the
13//! shape.
14//!
15//! # Reading a tree back
16//!
17//! The tree serializes, internally tagged (`{"type": "paragraph", …}`)
18//! so the shape maps one-to-one onto mdast. It is mostly an output: it
19//! is how to see what a frontend made of a manuscript. It reads back
20//! too, which is the door a host with a structured source of its own
21//! comes in through, so what the engine writes is what a host may hand
22//! it again.
23//!
24//! # Naming a node
25//!
26//! Every section, block and inline carries [`Attributes`]: any number of
27//! classes and at most one id, which is what a sheet reaches one
28//! element by. They are empty unless something set them, and a
29//! frontend is not the only thing that can: a host with a structured
30//! source of its own sets them on the tree it builds.
31//!
32//! # Node identity
33//!
34//! `NodeId` is engine-assigned, never frontend-supplied: input can't
35//! collide ids or forge diagnostic origins. Every node's `id` field is
36//! `#[serde(skip)]`, so a serialized tree has none. The ids in a tree
37//! built by hand are `NodeId::UNASSIGNED` until `Book::assign_node_ids`
38//! assigns dense ids from 1 in document order (pre-order: a node before
39//! its children, sections in reading order).
40//!
41//! # Source positions
42//!
43//! Every node has an optional 1-based line/column into the markdown
44//! source the frontend read it from; the section's `source` names the
45//! file. `origin` formats the pair for diagnostics
46//! (`chapter-01.md:12:3`). A missing position never fails a run.
47//!
48//! Beside it every node has an optional `span`: the bytes of that
49//! source the node was read from, markup included. [`Book::node_at`]
50//! turns a byte of a source into the node written there and
51//! [`Book::source_of`] turns a node back into the bytes it was read
52//! from, which is how a host holding the manuscript maps a cursor
53//! onto a page and a run under the pointer back onto the file it was
54//! written in. A tree built rather than parsed has neither, and both
55//! questions answer with nothing.
56
57use std::collections::BTreeMap;
58use std::ops::Range;
59
60use serde::{Deserialize, Serialize};
61
62mod anchor;
63
64pub use anchor::{Anchors, LinkTarget};
65
66/// Identity of one node in the content tree, for diagnostics and
67/// incremental relayout.
68///
69/// Assigned in document order, starting at 1.
70#[derive(
71    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
72)]
73pub struct NodeId(u32);
74
75impl NodeId {
76    /// What every node's id is before assignment.
77    pub const UNASSIGNED: NodeId = NodeId(0);
78
79    /// The raw id. Monotonic in document order within one book.
80    pub fn get(self) -> u32 {
81        self.0
82    }
83
84    /// The node one number names. Identity stays the engine's to
85    /// hand out; this is how a host asks about an id it read off a
86    /// display structure, and an id no node has answers with
87    /// nothing.
88    pub fn new(id: u32) -> NodeId {
89        NodeId(id)
90    }
91
92    /// The id this one becomes when the section around it is
93    /// renumbered by `step`. A section's nodes are dense and in
94    /// document order from its own id, so one step moves all of
95    /// them. Unassigned stays unassigned.
96    pub(crate) fn shifted(self, step: i64) -> NodeId {
97        if self == NodeId::UNASSIGNED {
98            return self;
99        }
100        if let Some((element, which)) = self.pseudo_element() {
101            return element.shifted(step).pseudo(which);
102        }
103        NodeId((self.0 as i64 + step).max(0) as u32)
104    }
105
106    /// The id of one pseudo-element of this element. No content node
107    /// has it.
108    pub(crate) fn pseudo(self, which: PseudoElement) -> NodeId {
109        let kind = match which {
110            PseudoElement::Before => 0,
111            PseudoElement::After => 1,
112            PseudoElement::FirstLetter => 2,
113            PseudoElement::FirstLine => 3,
114        };
115        NodeId(PSEUDO | (self.0 << 2) | kind)
116    }
117
118    /// The element and the pseudo-element this id names, where it
119    /// names one. Nothing for the id of a content node.
120    pub fn pseudo_element(self) -> Option<(NodeId, PseudoElement)> {
121        if self.0 & PSEUDO == 0 {
122            return None;
123        }
124        let which = match self.0 & 3 {
125            0 => PseudoElement::Before,
126            1 => PseudoElement::After,
127            2 => PseudoElement::FirstLetter,
128            _ => PseudoElement::FirstLine,
129        };
130        Some((NodeId((self.0 & !PSEUDO) >> 2), which))
131    }
132
133    /// The element this id stands for: the element a pseudo-element
134    /// belongs to, or the node itself.
135    pub fn element(self) -> NodeId {
136        self.pseudo_element().map_or(self, |(element, _)| element)
137    }
138}
139
140/// The ids at and above this one name pseudo-elements. Content ids
141/// count up from 1 and stay below 2^29, so the element and two bits
142/// for the kind fit under it.
143const PSEUDO: u32 = 1 << 31;
144
145/// The pseudo-elements the engine styles.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
147pub enum PseudoElement {
148    /// `::before`: on a block, a box that is its first child. On an
149    /// inline element, text before its own.
150    Before,
151    /// `::after`: on a block, a box that is its last child. On an
152    /// inline element, text after its own.
153    After,
154    /// `::first-letter`: the initial letter a drop cap is set from.
155    FirstLetter,
156    /// `::first-line`: the line a paragraph opens on.
157    FirstLine,
158}
159
160/// A stretch of one node's text: the node it was written in, and
161/// the bytes of that node's own text the stretch covers.
162///
163/// The range indexes the node's text as the frontend read it, before
164/// `text-transform` or a synthesized small capital changed what was
165/// shaped.
166#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
167pub struct SourceRange {
168    /// The node the text was written in.
169    pub node: NodeId,
170    /// Byte range in that node's own text.
171    pub range: Range<u32>,
172}
173
174/// The bytes of one source a node was read from: its extent in the
175/// file, markup included.
176///
177/// A source and the text of the nodes read from it are different
178/// bytes, because markup is not text. The span is the node's extent
179/// rather than a character-by-character map, so a byte of the source
180/// lands on the node written there and not on a letter of it.
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
182pub struct SourceSpan {
183    /// First byte of the source the node was read from.
184    pub start: u32,
185    /// One past the last.
186    pub end: u32,
187}
188
189impl SourceSpan {
190    /// Whether a byte of the source falls in the span. The end is
191    /// past it, so the spans of two nodes written one after the other
192    /// answer for their own bytes and no others.
193    pub fn covers(self, byte: u32) -> bool {
194        (self.start..self.end).contains(&byte)
195    }
196
197    /// How many bytes of the source it covers.
198    fn width(self) -> u32 {
199        self.end.saturating_sub(self.start)
200    }
201}
202
203/// What a sheet names one node by: any number of classes, at most
204/// one id.
205///
206/// Every section, block and inline carries one, empty unless
207/// something set it. The names are as they are written in CSS, without the `.` or
208/// the `#`.
209#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
210pub struct Attributes {
211    /// The id, which the sheet reaches with `#name`.
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub id: Option<String>,
214    /// The classes, in the order they were written, which the sheet
215    /// reaches with `.name`.
216    #[serde(default, skip_serializing_if = "Vec::is_empty")]
217    pub classes: Vec<String>,
218}
219
220impl Attributes {
221    /// Whether it names nothing, which is what a node carries until
222    /// something sets one.
223    pub fn is_empty(&self) -> bool {
224        self.id.is_none() && self.classes.is_empty()
225    }
226}
227
228/// The classes and the ids some part of a book carries, each once and
229/// in sorted order, as a sheet names them: without the `.` or the `#`.
230#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
231pub struct Names {
232    /// Every class.
233    pub classes: Vec<String>,
234    /// Every id.
235    pub ids: Vec<String>,
236}
237
238/// A 1-based position in the frontend's source document.
239///
240/// Line and column are as the markdown parser reported them. This is
241/// diagnostic data, never layout input.
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
243pub struct SourcePos {
244    /// 1-based line in the source markdown.
245    pub line: u32,
246    /// 1-based column in the source markdown.
247    pub column: u32,
248}
249
250/// Formats a file name plus a position for diagnostics:
251/// `chapter-01.md:12:3`. Missing parts degrade: bare file name, bare
252/// position, empty string.
253pub fn origin(source: Option<&str>, position: Option<SourcePos>) -> String {
254    match (source, position) {
255        (Some(file), Some(pos)) => format!("{file}:{}:{}", pos.line, pos.column),
256        (Some(file), None) => file.to_string(),
257        (None, Some(pos)) => format!("{}:{}", pos.line, pos.column),
258        (None, None) => String::new(),
259    }
260}
261
262/// Book metadata: everything about the work that isn't content.
263#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
264pub struct Metadata {
265    /// Title, for the half-title and running heads.
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub title: Option<String>,
268    /// Author, for the title page.
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub author: Option<String>,
271    /// Frontend-defined extensions (language, ISBN, subtitle…) keyed
272    /// by name. Opaque to the engine; style reads them, layout
273    /// doesn't.
274    ///
275    /// Left out of the JSON when empty, and so it has to be
276    /// optional coming back in: what the engine writes is what a
277    /// host hands it again.
278    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
279    pub extra: BTreeMap<String, String>,
280}
281
282impl Metadata {
283    /// The BCP 47 tag under `extra["language"]`. The PDF records it
284    /// and hyphenation takes its patterns from it. A blank tag counts
285    /// as none declared.
286    pub fn language(&self) -> Option<&str> {
287        self.extra
288            .get("language")
289            .map(|tag| tag.trim())
290            .filter(|tag| !tag.is_empty())
291    }
292}
293
294/// The root of the content tree: one book.
295#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
296pub struct Book {
297    /// The work's title, author and frontend extensions.
298    pub metadata: Metadata,
299    /// The chapters/files, in reading order.
300    pub sections: Vec<Section>,
301}
302
303/// A chapter or file: the unit of markdown input and of source
304/// attribution for diagnostics.
305#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
306pub struct Section {
307    /// Engine-assigned identity, for diagnostics; never serialized.
308    #[serde(skip)]
309    pub id: NodeId,
310    /// File the frontend read (e.g. `chapter-01.md`).
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub source: Option<String>,
313    /// Section title supplied outside the body (frontmatter
314    /// `title:`); implies heading level 1.
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub title: Option<String>,
317    /// The classes and id a sheet names the section by: from a chapter
318    /// file's frontmatter, or set by a host beside the source, where no
319    /// byte of the source moves.
320    #[serde(default, skip_serializing_if = "Attributes::is_empty")]
321    pub attributes: Attributes,
322    /// The section's blocks, in reading order.
323    pub blocks: Vec<Block>,
324    /// Where the frontend read this from.
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub position: Option<SourcePos>,
327    /// The bytes of that source it was read from.
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub span: Option<SourceSpan>,
330}
331
332/// A block-level element: the unit of fragmentation input.
333#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
334#[serde(tag = "type", rename_all = "snake_case")]
335pub enum Block {
336    /// `#` through `######`; levels outside 1–6 are rejected at parse.
337    Heading {
338        /// Engine-assigned identity, for diagnostics; never serialized.
339        #[serde(skip)]
340        id: NodeId,
341        /// `#` count, 1-6.
342        level: HeadingLevel,
343        /// The heading's text, in reading order.
344        inlines: Vec<Inline>,
345        /// What a sheet names it by.
346        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
347        attributes: Attributes,
348        /// Where the frontend read this from.
349        #[serde(skip_serializing_if = "Option::is_none")]
350        position: Option<SourcePos>,
351        /// The bytes of that source it was read from.
352        #[serde(skip_serializing_if = "Option::is_none")]
353        span: Option<SourceSpan>,
354    },
355    /// A run of prose: the unit line layout breaks.
356    Paragraph {
357        /// Engine-assigned identity, for diagnostics; never serialized.
358        #[serde(skip)]
359        id: NodeId,
360        /// The paragraph's text, in reading order.
361        inlines: Vec<Inline>,
362        /// What a sheet names it by.
363        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
364        attributes: Attributes,
365        /// Where the frontend read this from.
366        #[serde(skip_serializing_if = "Option::is_none")]
367        position: Option<SourcePos>,
368        /// The bytes of that source it was read from.
369        #[serde(skip_serializing_if = "Option::is_none")]
370        span: Option<SourceSpan>,
371    },
372    /// A quotation set off by `>`; contents are blocks, not inlines —
373    /// blockquotes nest.
374    Blockquote {
375        /// Engine-assigned identity, for diagnostics; never serialized.
376        #[serde(skip)]
377        id: NodeId,
378        /// The quoted blocks, in reading order.
379        blocks: Vec<Block>,
380        /// What a sheet names it by.
381        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
382        attributes: Attributes,
383        /// Where the frontend read this from.
384        #[serde(skip_serializing_if = "Option::is_none")]
385        position: Option<SourcePos>,
386        /// The bytes of that source it was read from.
387        #[serde(skip_serializing_if = "Option::is_none")]
388        span: Option<SourceSpan>,
389    },
390    /// A code block: preformatted text, set as it was written.
391    ///
392    /// It holds text rather than inlines, because a code block has no
393    /// markup inside it. The text keeps the newlines and the spaces
394    /// the author wrote, and those are what its lines break at.
395    CodeBlock {
396        /// Engine-assigned identity, for diagnostics; never serialized.
397        #[serde(skip)]
398        id: NodeId,
399        /// The word after the opening fence, where one was written.
400        /// It is carried for a painter that reads it, and the engine
401        /// makes nothing of it.
402        #[serde(default, skip_serializing_if = "Option::is_none")]
403        info: Option<String>,
404        /// The block's text, newlines and indentation intact.
405        text: String,
406        /// What a sheet names it by.
407        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
408        attributes: Attributes,
409        /// Where the frontend read this from.
410        #[serde(skip_serializing_if = "Option::is_none")]
411        position: Option<SourcePos>,
412        /// The bytes of that source it was read from.
413        #[serde(skip_serializing_if = "Option::is_none")]
414        span: Option<SourceSpan>,
415    },
416    /// `---`: a scene break, rendered as space or an ornament (❦).
417    ThematicBreak {
418        /// Engine-assigned identity, for diagnostics; never serialized.
419        #[serde(skip)]
420        id: NodeId,
421        /// What a sheet names it by.
422        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
423        attributes: Attributes,
424        /// Where the frontend read this from.
425        #[serde(skip_serializing_if = "Option::is_none")]
426        position: Option<SourcePos>,
427        /// The bytes of that source it was read from.
428        #[serde(skip_serializing_if = "Option::is_none")]
429        span: Option<SourceSpan>,
430    },
431    /// `\pagebreak`: what follows it starts a new page. It holds no
432    /// text and takes no height.
433    PageBreak {
434        /// Engine-assigned identity, for diagnostics; never serialized.
435        #[serde(skip)]
436        id: NodeId,
437        /// What a sheet names it by.
438        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
439        attributes: Attributes,
440        /// Where the frontend read this from.
441        #[serde(skip_serializing_if = "Option::is_none")]
442        position: Option<SourcePos>,
443        /// The bytes of that source it was read from.
444        #[serde(skip_serializing_if = "Option::is_none")]
445        span: Option<SourceSpan>,
446    },
447    /// `\columnbreak`: what follows it starts the next column. It
448    /// holds no text and takes no height.
449    ColumnBreak {
450        /// Engine-assigned identity, for diagnostics; never serialized.
451        #[serde(skip)]
452        id: NodeId,
453        /// What a sheet names it by.
454        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
455        attributes: Attributes,
456        /// Where the frontend read this from.
457        #[serde(skip_serializing_if = "Option::is_none")]
458        position: Option<SourcePos>,
459        /// The bytes of that source it was read from.
460        #[serde(skip_serializing_if = "Option::is_none")]
461        span: Option<SourceSpan>,
462    },
463    /// A block-level image.
464    Image {
465        /// Engine-assigned identity, for diagnostics; never serialized.
466        #[serde(skip)]
467        id: NodeId,
468        /// Where the image lives; the host resolves it, not the engine.
469        url: String,
470        /// Alt text: not laid out, but part of the accessibility
471        /// contract.
472        alt: String,
473        /// What a sheet names it by.
474        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
475        attributes: Attributes,
476        /// Where the frontend read this from.
477        #[serde(skip_serializing_if = "Option::is_none")]
478        position: Option<SourcePos>,
479        /// The bytes of that source it was read from.
480        #[serde(skip_serializing_if = "Option::is_none")]
481        span: Option<SourceSpan>,
482    },
483    /// A list of items, numbered or not. An item holds blocks, as a
484    /// blockquote does, so a list nests inside an item of another.
485    List {
486        /// Engine-assigned identity, for diagnostics; never serialized.
487        #[serde(skip)]
488        id: NodeId,
489        /// Whether the items are numbered.
490        #[serde(default)]
491        ordered: bool,
492        /// The number of the first item of a numbered list.
493        #[serde(default = "first_number", skip_serializing_if = "is_first_number")]
494        start: u32,
495        /// Whether the source wrote the items without blank lines
496        /// between them: a list of phrases rather than a list of
497        /// paragraphs.
498        #[serde(default)]
499        tight: bool,
500        /// The items, in reading order.
501        #[serde(default)]
502        items: Vec<ListItem>,
503        /// What a sheet names it by.
504        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
505        attributes: Attributes,
506        /// Where the frontend read this from.
507        #[serde(skip_serializing_if = "Option::is_none")]
508        position: Option<SourcePos>,
509        /// The bytes of that source it was read from.
510        #[serde(skip_serializing_if = "Option::is_none")]
511        span: Option<SourceSpan>,
512    },
513    /// A grid of cells. A cell holds blocks, as a blockquote does.
514    Table {
515        /// Engine-assigned identity, for diagnostics; never serialized.
516        #[serde(skip)]
517        id: NodeId,
518        /// The header rows, which name the columns, in reading order.
519        #[serde(default)]
520        head: Vec<Row>,
521        /// The body rows, in reading order.
522        #[serde(default)]
523        body: Vec<Row>,
524        /// What a sheet names it by.
525        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
526        attributes: Attributes,
527        /// Where the frontend read this from.
528        #[serde(skip_serializing_if = "Option::is_none")]
529        position: Option<SourcePos>,
530        /// The bytes of that source it was read from.
531        #[serde(skip_serializing_if = "Option::is_none")]
532        span: Option<SourceSpan>,
533    },
534}
535
536/// A heading level: 1 to 6, the range markdown defines.
537#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
538#[serde(into = "u8", try_from = "u8")]
539pub enum HeadingLevel {
540    /// `#`
541    H1,
542    /// `##`
543    H2,
544    /// `###`
545    H3,
546    /// `####`
547    H4,
548    /// `#####`
549    H5,
550    /// `######`
551    H6,
552}
553
554impl From<HeadingLevel> for u8 {
555    fn from(level: HeadingLevel) -> u8 {
556        match level {
557            HeadingLevel::H1 => 1,
558            HeadingLevel::H2 => 2,
559            HeadingLevel::H3 => 3,
560            HeadingLevel::H4 => 4,
561            HeadingLevel::H5 => 5,
562            HeadingLevel::H6 => 6,
563        }
564    }
565}
566
567impl TryFrom<u8> for HeadingLevel {
568    type Error = InvalidHeadingLevel;
569
570    fn try_from(value: u8) -> Result<Self, Self::Error> {
571        match value {
572            1 => Ok(HeadingLevel::H1),
573            2 => Ok(HeadingLevel::H2),
574            3 => Ok(HeadingLevel::H3),
575            4 => Ok(HeadingLevel::H4),
576            5 => Ok(HeadingLevel::H5),
577            6 => Ok(HeadingLevel::H6),
578            _ => Err(InvalidHeadingLevel(value)),
579        }
580    }
581}
582
583/// A heading level outside 1–6, with the offending value.
584#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
585#[error("heading level must be 1-6, got {0}")]
586pub struct InvalidHeadingLevel(pub u8);
587
588/// What a numbered list counts from when the source names no start.
589fn first_number() -> u32 {
590    1
591}
592
593fn is_first_number(start: &u32) -> bool {
594    *start == 1
595}
596
597/// One item of a list.
598#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
599pub struct ListItem {
600    /// Engine-assigned identity, for diagnostics; never serialized.
601    #[serde(skip)]
602    pub id: NodeId,
603    /// The item's content, in reading order. An empty item has none.
604    #[serde(default)]
605    pub blocks: Vec<Block>,
606    /// What a sheet names it by.
607    #[serde(default, skip_serializing_if = "Attributes::is_empty")]
608    pub attributes: Attributes,
609    /// Where the frontend read this from.
610    #[serde(skip_serializing_if = "Option::is_none")]
611    pub position: Option<SourcePos>,
612    /// The bytes of that source it was read from.
613    #[serde(skip_serializing_if = "Option::is_none")]
614    pub span: Option<SourceSpan>,
615}
616
617/// The blocks of every item of a list, in reading order.
618pub fn item_blocks(items: &[ListItem]) -> impl Iterator<Item = &[Block]> {
619    items.iter().map(|item| item.blocks.as_slice())
620}
621
622/// One row of a table.
623#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
624pub struct Row {
625    /// Engine-assigned identity, for diagnostics; never serialized.
626    #[serde(skip)]
627    pub id: NodeId,
628    /// The row's cells, from the leading edge. A row with fewer cells
629    /// than the table has columns leaves the rest of them empty.
630    #[serde(default)]
631    pub cells: Vec<Cell>,
632    /// What a sheet names it by.
633    #[serde(default, skip_serializing_if = "Attributes::is_empty")]
634    pub attributes: Attributes,
635    /// Where the frontend read this from.
636    #[serde(skip_serializing_if = "Option::is_none")]
637    pub position: Option<SourcePos>,
638    /// The bytes of that source it was read from.
639    #[serde(skip_serializing_if = "Option::is_none")]
640    pub span: Option<SourceSpan>,
641}
642
643/// One cell of a table row.
644#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
645pub struct Cell {
646    /// Engine-assigned identity, for diagnostics; never serialized.
647    #[serde(skip)]
648    pub id: NodeId,
649    /// The cell's content, in reading order. An empty cell has none.
650    #[serde(default)]
651    pub blocks: Vec<Block>,
652    /// The alignment the source wrote on the cell's column, if it
653    /// wrote one.
654    #[serde(default, skip_serializing_if = "Option::is_none")]
655    pub align: Option<Alignment>,
656    /// What a sheet names it by.
657    #[serde(default, skip_serializing_if = "Attributes::is_empty")]
658    pub attributes: Attributes,
659    /// Where the frontend read this from.
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub position: Option<SourcePos>,
662    /// The bytes of that source it was read from.
663    #[serde(skip_serializing_if = "Option::is_none")]
664    pub span: Option<SourceSpan>,
665}
666
667/// The alignment a table's delimiter row writes on a column.
668#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
669#[serde(rename_all = "snake_case")]
670pub enum Alignment {
671    /// `:---`
672    Left,
673    /// `:---:`
674    Center,
675    /// `---:`
676    Right,
677}
678
679/// Every row of a table, the header rows first.
680pub fn rows<'a>(head: &'a [Row], body: &'a [Row]) -> impl Iterator<Item = &'a Row> {
681    head.iter().chain(body)
682}
683
684/// The blocks of every cell of a table, row by row, the header rows
685/// first.
686pub fn cell_blocks<'a>(head: &'a [Row], body: &'a [Row]) -> impl Iterator<Item = &'a [Block]> {
687    rows(head, body).flat_map(|row| row.cells.iter().map(|cell| cell.blocks.as_slice()))
688}
689
690/// An inline element: participates in line layout.
691#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
692#[serde(tag = "type", rename_all = "snake_case")]
693pub enum Inline {
694    /// A run of text. The frontend has already decoded entities; the
695    /// engine sees plain Unicode.
696    Text {
697        /// Engine-assigned identity, for diagnostics; never serialized.
698        #[serde(skip)]
699        id: NodeId,
700        /// The characters themselves, entities already decoded.
701        value: String,
702        /// What a sheet names it by.
703        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
704        attributes: Attributes,
705        /// Where the frontend read this from.
706        #[serde(skip_serializing_if = "Option::is_none")]
707        position: Option<SourcePos>,
708        /// The bytes of that source it was read from.
709        #[serde(skip_serializing_if = "Option::is_none")]
710        span: Option<SourceSpan>,
711    },
712    /// A hard line break: the line ends here, and what follows stays
713    /// in the same block. It holds no text.
714    Break {
715        /// Engine-assigned identity, for diagnostics; never serialized.
716        #[serde(skip)]
717        id: NodeId,
718        /// What a sheet names it by.
719        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
720        attributes: Attributes,
721        /// Where the frontend read this from.
722        #[serde(skip_serializing_if = "Option::is_none")]
723        position: Option<SourcePos>,
724        /// The bytes of that source it was read from.
725        #[serde(skip_serializing_if = "Option::is_none")]
726        span: Option<SourceSpan>,
727    },
728    /// `*emphasis*`: italic, in the default sheet.
729    Emphasis {
730        /// Engine-assigned identity, for diagnostics; never serialized.
731        #[serde(skip)]
732        id: NodeId,
733        /// The emphasised inlines.
734        children: Vec<Inline>,
735        /// What a sheet names it by.
736        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
737        attributes: Attributes,
738        /// Where the frontend read this from.
739        #[serde(skip_serializing_if = "Option::is_none")]
740        position: Option<SourcePos>,
741        /// The bytes of that source it was read from.
742        #[serde(skip_serializing_if = "Option::is_none")]
743        span: Option<SourceSpan>,
744    },
745    /// `**strong**`: bold, in the default sheet.
746    Strong {
747        /// Engine-assigned identity, for diagnostics; never serialized.
748        #[serde(skip)]
749        id: NodeId,
750        /// The strengthened inlines.
751        children: Vec<Inline>,
752        /// What a sheet names it by.
753        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
754        attributes: Attributes,
755        /// Where the frontend read this from.
756        #[serde(skip_serializing_if = "Option::is_none")]
757        position: Option<SourcePos>,
758        /// The bytes of that source it was read from.
759        #[serde(skip_serializing_if = "Option::is_none")]
760        span: Option<SourceSpan>,
761    },
762    /// `` `code` ``: monospace, and never hyphenated.
763    Code {
764        /// Engine-assigned identity, for diagnostics; never serialized.
765        #[serde(skip)]
766        id: NodeId,
767        /// The literal code text; no markup inside.
768        value: String,
769        /// What a sheet names it by.
770        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
771        attributes: Attributes,
772        /// Where the frontend read this from.
773        #[serde(skip_serializing_if = "Option::is_none")]
774        position: Option<SourcePos>,
775        /// The bytes of that source it was read from.
776        #[serde(skip_serializing_if = "Option::is_none")]
777        span: Option<SourceSpan>,
778    },
779    /// A hyperlink. The text lays out; the url is for painters that can
780    /// express one.
781    Link {
782        /// Engine-assigned identity, for diagnostics; never serialized.
783        #[serde(skip)]
784        id: NodeId,
785        /// The link target.
786        url: String,
787        /// The linked inlines.
788        children: Vec<Inline>,
789        /// What a sheet names it by.
790        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
791        attributes: Attributes,
792        /// Where the frontend read this from.
793        #[serde(skip_serializing_if = "Option::is_none")]
794        position: Option<SourcePos>,
795        /// The bytes of that source it was read from.
796        #[serde(skip_serializing_if = "Option::is_none")]
797        span: Option<SourceSpan>,
798    },
799    /// A footnote. Its reference sits in the line where it was
800    /// written, and its blocks are set at the foot of the page that
801    /// reference lands on.
802    ///
803    /// The blocks are the note itself, so a note holds paragraphs,
804    /// lists and everything else a blockquote holds. The engine sets
805    /// the reference and the number beside the note from the
806    /// cascade, so neither is in the tree.
807    Note {
808        /// Engine-assigned identity, for diagnostics; never serialized.
809        #[serde(skip)]
810        id: NodeId,
811        /// The note itself, in reading order.
812        blocks: Vec<Block>,
813        /// What a sheet names it by.
814        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
815        attributes: Attributes,
816        /// Where the frontend read this from.
817        #[serde(skip_serializing_if = "Option::is_none")]
818        position: Option<SourcePos>,
819        /// The bytes of that source it was read from.
820        #[serde(skip_serializing_if = "Option::is_none")]
821        span: Option<SourceSpan>,
822    },
823    /// `~~struck~~`: a line through it, in the default sheet.
824    Strikethrough {
825        /// Engine-assigned identity, for diagnostics; never serialized.
826        #[serde(skip)]
827        id: NodeId,
828        /// The struck inlines.
829        children: Vec<Inline>,
830        /// What a sheet names it by.
831        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
832        attributes: Attributes,
833        /// Where the frontend read this from.
834        #[serde(skip_serializing_if = "Option::is_none")]
835        position: Option<SourcePos>,
836        /// The bytes of that source it was read from.
837        #[serde(skip_serializing_if = "Option::is_none")]
838        span: Option<SourceSpan>,
839    },
840    /// A run the sheet names, and nothing else. It carries no meaning
841    /// of its own, so the built-in sheet styles it like the text
842    /// around it and a class on it is what reaches it.
843    Span {
844        /// Engine-assigned identity, for diagnostics; never serialized.
845        #[serde(skip)]
846        id: NodeId,
847        /// The inlines inside the run.
848        children: Vec<Inline>,
849        /// What a sheet names it by.
850        #[serde(default, skip_serializing_if = "Attributes::is_empty")]
851        attributes: Attributes,
852        /// Where the frontend read this from.
853        #[serde(skip_serializing_if = "Option::is_none")]
854        position: Option<SourcePos>,
855        /// The bytes of that source it was read from.
856        #[serde(skip_serializing_if = "Option::is_none")]
857        span: Option<SourceSpan>,
858    },
859}
860
861/// The text of an inline tree, markup discarded: every inline run
862/// together and a hard break as a newline, as `content()` reads an
863/// element and as a frontend reads alt text.
864pub fn text(inlines: &[Inline]) -> String {
865    let mut out = String::new();
866    push_text(inlines, &mut out);
867    out
868}
869
870fn push_text(inlines: &[Inline], out: &mut String) {
871    for inline in inlines {
872        match inline {
873            Inline::Text { value, .. } | Inline::Code { value, .. } => out.push_str(value),
874            Inline::Break { .. } => out.push('\n'),
875            // A note is set at the foot of the page, so its prose is
876            // no part of the text the line it was written in holds.
877            Inline::Note { .. } => {}
878            Inline::Emphasis { children, .. }
879            | Inline::Strong { children, .. }
880            | Inline::Link { children, .. }
881            | Inline::Strikethrough { children, .. }
882            | Inline::Span { children, .. } => push_text(children, out),
883        }
884    }
885}
886
887impl Book {
888    /// Assign ids to every node, in document order (pre-order: a node
889    /// before its children, sections in reading order), starting at 1.
890    /// Runs once, after deserialization; running it again renumbers.
891    pub fn assign_node_ids(&mut self) {
892        let mut next = 1u32;
893        for section in &mut self.sections {
894            section.id = next_id(&mut next);
895            for block in &mut section.blocks {
896                assign_block(block, &mut next);
897            }
898        }
899    }
900
901    /// The node one byte of one source was read into: the innermost,
902    /// so a byte of prose answers with the run it was typed into and
903    /// a byte of markup answers with the construct it opens.
904    ///
905    /// Only sections read from that source are looked at, so one
906    /// file's cursor is answered by one file's nodes. Nothing for a
907    /// byte no node was read from, such as a blank line between
908    /// chapters or a source's frontmatter, and nothing for a tree
909    /// built rather than parsed.
910    pub fn node_at(&self, source: &str, byte: u32) -> Option<NodeId> {
911        self.sections
912            .iter()
913            .filter(|section| section.source.as_deref() == Some(source))
914            .find_map(|section| {
915                let span = section.span.filter(|span| span.covers(byte))?;
916                let inner = node_in_blocks(&section.blocks, byte);
917                let inner = match node_in_notes(&section.blocks, byte) {
918                    Some(note) => Some(narrowest(note, inner)),
919                    None => inner,
920                };
921                Some(narrowest((section.id, span), inner))
922            })
923            .map(|(node, _)| node)
924    }
925
926    /// The source a node was read from, and the bytes of it the node
927    /// covers.
928    ///
929    /// Nothing for a node the engine synthesized, or one from a tree
930    /// built rather than parsed: neither was read from anything.
931    pub fn source_of(&self, node: NodeId) -> Option<(&str, SourceSpan)> {
932        if node == NodeId::UNASSIGNED {
933            return None;
934        }
935        // A section's nodes are dense and in document order from the
936        // section's own id, so the section holding a node is the last
937        // one numbered at or before it.
938        let at = self
939            .sections
940            .partition_point(|section| section.id.get() <= node.get())
941            .checked_sub(1)?;
942        let section = &self.sections[at];
943        let source = section.source.as_deref()?;
944        let span = if section.id == node {
945            section.span?
946        } else {
947            span_in_blocks(&section.blocks, node)?
948        };
949        Some((source, span))
950    }
951
952    /// The classes and the ids the blocks and inlines of one source
953    /// carry, or of every source when `source` is `None`. A list
954    /// item, a table row or cell, and a note count as blocks and
955    /// inlines here.
956    ///
957    /// A section's own names are not in the answer. A host sets them
958    /// beside the source, or frontmatter writes them.
959    pub fn names(&self, source: Option<&str>) -> Names {
960        let mut classes = std::collections::BTreeSet::new();
961        let mut ids = std::collections::BTreeSet::new();
962        let mut add = |attributes: &Attributes| {
963            classes.extend(attributes.classes.iter().cloned());
964            ids.extend(attributes.id.iter().cloned());
965        };
966        for section in &self.sections {
967            if source.is_none_or(|source| section.source.as_deref() == Some(source)) {
968                names_in_blocks(&section.blocks, &mut add);
969            }
970        }
971        Names {
972            classes: classes.into_iter().collect(),
973            ids: ids.into_iter().collect(),
974        }
975    }
976
977    /// The ids one node and its descendants hold.
978    ///
979    /// Ids are assigned in document order, a node before its
980    /// children, so what a node covers is a run of consecutive
981    /// numbers. Asking what a node was set from is asking about
982    /// every id in this range.
983    ///
984    /// Nothing for a node the book does not hold.
985    pub fn subtree(&self, node: NodeId) -> Option<Range<u32>> {
986        if node == NodeId::UNASSIGNED {
987            return None;
988        }
989        let at = self
990            .sections
991            .partition_point(|section| section.id.get() <= node.get())
992            .checked_sub(1)?;
993        let section = &self.sections[at];
994        let first = section.id.get();
995        let count = 1 + section.blocks.iter().map(block_nodes).sum::<u32>();
996        if section.id == node {
997            return Some(first..first + count);
998        }
999        if !(first..first + count).contains(&node.get()) {
1000            return None;
1001        }
1002        subtree_in_blocks(&section.blocks, node)
1003    }
1004}
1005
1006/// Hands the names of every block and inline under these blocks to
1007/// `add`.
1008fn names_in_blocks(blocks: &[Block], add: &mut impl FnMut(&Attributes)) {
1009    for block in blocks {
1010        add(block_attributes(block));
1011        match block {
1012            Block::Heading { inlines, .. } | Block::Paragraph { inlines, .. } => {
1013                names_in_inlines(inlines, add)
1014            }
1015            Block::Blockquote { blocks, .. } => names_in_blocks(blocks, add),
1016            Block::List { items, .. } => {
1017                for item in items {
1018                    add(&item.attributes);
1019                    names_in_blocks(&item.blocks, add);
1020                }
1021            }
1022            Block::Table { head, body, .. } => {
1023                for row in rows(head, body) {
1024                    add(&row.attributes);
1025                    for cell in &row.cells {
1026                        add(&cell.attributes);
1027                        names_in_blocks(&cell.blocks, add);
1028                    }
1029                }
1030            }
1031            Block::CodeBlock { .. }
1032            | Block::ThematicBreak { .. }
1033            | Block::PageBreak { .. }
1034            | Block::ColumnBreak { .. }
1035            | Block::Image { .. } => {}
1036        }
1037    }
1038}
1039
1040/// The same, over inlines.
1041fn names_in_inlines(inlines: &[Inline], add: &mut impl FnMut(&Attributes)) {
1042    for inline in inlines {
1043        add(inline_attributes(inline));
1044        match inline {
1045            Inline::Text { .. } | Inline::Code { .. } | Inline::Break { .. } => {}
1046            Inline::Note { blocks, .. } => names_in_blocks(blocks, add),
1047            Inline::Emphasis { children, .. }
1048            | Inline::Strong { children, .. }
1049            | Inline::Link { children, .. }
1050            | Inline::Strikethrough { children, .. }
1051            | Inline::Span { children, .. } => names_in_inlines(children, add),
1052        }
1053    }
1054}
1055
1056/// The ids one node of these blocks holds, by id. Ids are dense, so
1057/// the block holding a node is the one whose own run of numbers
1058/// covers it.
1059fn subtree_in_blocks(blocks: &[Block], node: NodeId) -> Option<Range<u32>> {
1060    for block in blocks {
1061        let first = block_id(block).get();
1062        let held = first..first + block_nodes(block);
1063        if !held.contains(&node.get()) {
1064            continue;
1065        }
1066        if block_id(block) == node {
1067            return Some(held);
1068        }
1069        return match block {
1070            Block::Heading { inlines, .. } | Block::Paragraph { inlines, .. } => {
1071                subtree_in_inlines(inlines, node)
1072            }
1073            Block::Blockquote { blocks, .. } => subtree_in_blocks(blocks, node),
1074            Block::List { items, .. } => subtree_in_items(items, node),
1075            Block::Table { head, body, .. } => subtree_in_rows(rows(head, body), node),
1076            Block::CodeBlock { .. }
1077            | Block::ThematicBreak { .. }
1078            | Block::PageBreak { .. }
1079            | Block::ColumnBreak { .. }
1080            | Block::Image { .. } => None,
1081        };
1082    }
1083    None
1084}
1085
1086/// The same, over the items of one list.
1087fn subtree_in_items(items: &[ListItem], node: NodeId) -> Option<Range<u32>> {
1088    let item = items
1089        .iter()
1090        .find(|item| (item.id.get()..item.id.get() + item_nodes(item)).contains(&node.get()))?;
1091    if item.id == node {
1092        return Some(item.id.get()..item.id.get() + item_nodes(item));
1093    }
1094    subtree_in_blocks(&item.blocks, node)
1095}
1096
1097/// How many ids one list item holds, itself included.
1098fn item_nodes(item: &ListItem) -> u32 {
1099    1 + item.blocks.iter().map(block_nodes).sum::<u32>()
1100}
1101
1102/// The same, over the inlines of one block.
1103fn subtree_in_inlines(inlines: &[Inline], node: NodeId) -> Option<Range<u32>> {
1104    for inline in inlines {
1105        let first = inline_id(inline).get();
1106        let held = first..first + inline_nodes(inline);
1107        if !held.contains(&node.get()) {
1108            continue;
1109        }
1110        if inline_id(inline) == node {
1111            return Some(held);
1112        }
1113        return match inline {
1114            Inline::Text { .. } | Inline::Code { .. } | Inline::Break { .. } => None,
1115            Inline::Note { blocks, .. } => subtree_in_blocks(blocks, node),
1116            Inline::Emphasis { children, .. }
1117            | Inline::Strong { children, .. }
1118            | Inline::Link { children, .. }
1119            | Inline::Strikethrough { children, .. }
1120            | Inline::Span { children, .. } => subtree_in_inlines(children, node),
1121        };
1122    }
1123    None
1124}
1125
1126/// How many ids one block holds, itself included.
1127fn block_nodes(block: &Block) -> u32 {
1128    1 + match block {
1129        Block::Heading { inlines, .. } | Block::Paragraph { inlines, .. } => {
1130            inlines.iter().map(inline_nodes).sum()
1131        }
1132        Block::Blockquote { blocks, .. } => blocks.iter().map(block_nodes).sum(),
1133        Block::List { items, .. } => items.iter().map(item_nodes).sum(),
1134        Block::Table { head, body, .. } => rows(head, body).map(row_nodes).sum(),
1135        Block::CodeBlock { .. }
1136        | Block::ThematicBreak { .. }
1137        | Block::PageBreak { .. }
1138        | Block::ColumnBreak { .. }
1139        | Block::Image { .. } => 0,
1140    }
1141}
1142
1143/// The same, for one row of a table.
1144fn row_nodes(row: &Row) -> u32 {
1145    1 + row.cells.iter().map(cell_nodes).sum::<u32>()
1146}
1147
1148/// The same, for one cell.
1149fn cell_nodes(cell: &Cell) -> u32 {
1150    1 + cell.blocks.iter().map(block_nodes).sum::<u32>()
1151}
1152
1153/// The ids one node of a table's rows holds, by id.
1154fn subtree_in_rows<'a>(rows: impl Iterator<Item = &'a Row>, node: NodeId) -> Option<Range<u32>> {
1155    for row in rows {
1156        let held = row.id.get()..row.id.get() + row_nodes(row);
1157        if !held.contains(&node.get()) {
1158            continue;
1159        }
1160        if row.id == node {
1161            return Some(held);
1162        }
1163        for cell in &row.cells {
1164            let held = cell.id.get()..cell.id.get() + cell_nodes(cell);
1165            if !held.contains(&node.get()) {
1166                continue;
1167            }
1168            if cell.id == node {
1169                return Some(held);
1170            }
1171            return subtree_in_blocks(&cell.blocks, node);
1172        }
1173        return None;
1174    }
1175    None
1176}
1177
1178/// The same, for one inline.
1179pub(crate) fn inline_nodes(inline: &Inline) -> u32 {
1180    1 + match inline {
1181        Inline::Text { .. } | Inline::Code { .. } | Inline::Break { .. } => 0,
1182        Inline::Note { blocks, .. } => blocks.iter().map(block_nodes).sum(),
1183        Inline::Emphasis { children, .. }
1184        | Inline::Strong { children, .. }
1185        | Inline::Link { children, .. }
1186        | Inline::Strikethrough { children, .. }
1187        | Inline::Span { children, .. } => children.iter().map(inline_nodes).sum(),
1188    }
1189}
1190
1191/// The narrower of a node and whichever of its descendants was read
1192/// from the same byte.
1193fn narrowest(
1194    node: (NodeId, SourceSpan),
1195    inner: Option<(NodeId, SourceSpan)>,
1196) -> (NodeId, SourceSpan) {
1197    match inner {
1198        Some(inner) if inner.1.width() <= node.1.width() => inner,
1199        _ => node,
1200    }
1201}
1202
1203/// The innermost block or inline of these blocks a byte was read
1204/// into. Blocks are in source order, so a block starting past the
1205/// byte ends the search; an image written among prose is the one that
1206/// starts inside the paragraph it was moved out of, which is why the
1207/// narrowest span wins rather than the first.
1208fn node_in_blocks(blocks: &[Block], byte: u32) -> Option<(NodeId, SourceSpan)> {
1209    let mut found: Option<(NodeId, SourceSpan)> = None;
1210    for block in blocks {
1211        let Some(span) = block_span(block) else {
1212            continue;
1213        };
1214        if span.start > byte {
1215            break;
1216        }
1217        if !span.covers(byte) {
1218            continue;
1219        }
1220        let inner = match block {
1221            Block::Heading { inlines, .. } | Block::Paragraph { inlines, .. } => {
1222                node_in_inlines(inlines, byte)
1223            }
1224            Block::Blockquote { blocks, .. } => node_in_blocks(blocks, byte),
1225            Block::List { items, .. } => node_in_items(items, byte),
1226            Block::Table { head, body, .. } => node_in_rows(rows(head, body), byte),
1227            Block::CodeBlock { .. }
1228            | Block::ThematicBreak { .. }
1229            | Block::PageBreak { .. }
1230            | Block::ColumnBreak { .. }
1231            | Block::Image { .. } => None,
1232        };
1233        let hit = narrowest((block_id(block), span), inner);
1234        if found.is_none_or(|found| hit.1.width() < found.1.width()) {
1235            found = Some(hit);
1236        }
1237    }
1238    found
1239}
1240
1241/// The same, over the inlines of one block.
1242fn node_in_inlines(inlines: &[Inline], byte: u32) -> Option<(NodeId, SourceSpan)> {
1243    for inline in inlines {
1244        let Some(span) = inline_span(inline) else {
1245            continue;
1246        };
1247        if span.start > byte {
1248            break;
1249        }
1250        if !span.covers(byte) {
1251            continue;
1252        }
1253        let inner = match inline {
1254            // A note was written where its definition was, which is
1255            // not the stretch its reference covers. `node_in_notes`
1256            // answers for those bytes.
1257            Inline::Text { .. }
1258            | Inline::Code { .. }
1259            | Inline::Break { .. }
1260            | Inline::Note { .. } => None,
1261            Inline::Emphasis { children, .. }
1262            | Inline::Strong { children, .. }
1263            | Inline::Link { children, .. }
1264            | Inline::Strikethrough { children, .. }
1265            | Inline::Span { children, .. } => node_in_inlines(children, byte),
1266        };
1267        return Some(narrowest((inline_id(inline), span), inner));
1268    }
1269    None
1270}
1271
1272/// Every note written among these blocks, in reading order. A note
1273/// written inside another note comes after it.
1274pub fn notes_in_blocks(blocks: &[Block]) -> Vec<&Inline> {
1275    let mut out = Vec::new();
1276    gather_notes_in_blocks(blocks, &mut out);
1277    out
1278}
1279
1280/// The notes written directly among these inlines, in reading order.
1281/// A note written inside another note is that note's own.
1282pub fn notes_in_inlines(inlines: &[Inline]) -> Vec<&Inline> {
1283    let mut out = Vec::new();
1284    shallow_notes_in_inlines(inlines, &mut out);
1285    out
1286}
1287
1288fn shallow_notes_in_inlines<'b>(inlines: &'b [Inline], out: &mut Vec<&'b Inline>) {
1289    for inline in inlines {
1290        match inline {
1291            Inline::Note { .. } => out.push(inline),
1292            Inline::Emphasis { children, .. }
1293            | Inline::Strong { children, .. }
1294            | Inline::Link { children, .. }
1295            | Inline::Strikethrough { children, .. }
1296            | Inline::Span { children, .. } => shallow_notes_in_inlines(children, out),
1297            Inline::Text { .. } | Inline::Code { .. } | Inline::Break { .. } => {}
1298        }
1299    }
1300}
1301
1302fn gather_notes_in_blocks<'b>(blocks: &'b [Block], out: &mut Vec<&'b Inline>) {
1303    for block in blocks {
1304        match block {
1305            Block::Heading { inlines, .. } | Block::Paragraph { inlines, .. } => {
1306                gather_notes_in_inlines(inlines, out)
1307            }
1308            Block::Blockquote { blocks, .. } => gather_notes_in_blocks(blocks, out),
1309            Block::List { items, .. } => {
1310                for item in items {
1311                    gather_notes_in_blocks(&item.blocks, out);
1312                }
1313            }
1314            Block::Table { head, body, .. } => {
1315                for blocks in cell_blocks(head, body) {
1316                    gather_notes_in_blocks(blocks, out);
1317                }
1318            }
1319            Block::CodeBlock { .. }
1320            | Block::ThematicBreak { .. }
1321            | Block::PageBreak { .. }
1322            | Block::ColumnBreak { .. }
1323            | Block::Image { .. } => {}
1324        }
1325    }
1326}
1327
1328fn gather_notes_in_inlines<'b>(inlines: &'b [Inline], out: &mut Vec<&'b Inline>) {
1329    for inline in inlines {
1330        match inline {
1331            Inline::Note { blocks, .. } => {
1332                out.push(inline);
1333                gather_notes_in_blocks(blocks, out);
1334            }
1335            Inline::Emphasis { children, .. }
1336            | Inline::Strong { children, .. }
1337            | Inline::Link { children, .. }
1338            | Inline::Strikethrough { children, .. }
1339            | Inline::Span { children, .. } => gather_notes_in_inlines(children, out),
1340            Inline::Text { .. } | Inline::Code { .. } | Inline::Break { .. } => {}
1341        }
1342    }
1343}
1344
1345/// The innermost node of the notes written among these blocks a
1346/// byte was read into.
1347///
1348/// A note sits in the line its reference was written on, and its own
1349/// blocks were read from wherever the note was written. So the bytes
1350/// of a note are outside the span of every node that holds it, and
1351/// the search for them starts again here.
1352fn node_in_notes(blocks: &[Block], byte: u32) -> Option<(NodeId, SourceSpan)> {
1353    let mut found: Option<(NodeId, SourceSpan)> = None;
1354    for note in notes_in_blocks(blocks) {
1355        let Inline::Note { id, blocks, .. } = note else {
1356            continue;
1357        };
1358        let Some(hit) = node_in_blocks(blocks, byte) else {
1359            continue;
1360        };
1361        let hit = match inline_span(note) {
1362            Some(span) if span.covers(byte) => narrowest((*id, span), Some(hit)),
1363            _ => hit,
1364        };
1365        if found.is_none_or(|found| hit.1.width() < found.1.width()) {
1366            found = Some(hit);
1367        }
1368    }
1369    found
1370}
1371
1372/// The span of one node of these blocks, by id.
1373fn span_in_blocks(blocks: &[Block], node: NodeId) -> Option<SourceSpan> {
1374    for block in blocks {
1375        if block_id(block) == node {
1376            return block_span(block);
1377        }
1378        let found = match block {
1379            Block::Heading { inlines, .. } | Block::Paragraph { inlines, .. } => {
1380                span_in_inlines(inlines, node)
1381            }
1382            Block::Blockquote { blocks, .. } => span_in_blocks(blocks, node),
1383            Block::List { items, .. } => span_in_items(items, node),
1384            Block::Table { head, body, .. } => span_in_rows(rows(head, body), node),
1385            Block::CodeBlock { .. }
1386            | Block::ThematicBreak { .. }
1387            | Block::PageBreak { .. }
1388            | Block::ColumnBreak { .. }
1389            | Block::Image { .. } => None,
1390        };
1391        if found.is_some() {
1392            return found;
1393        }
1394    }
1395    None
1396}
1397
1398/// The innermost item, block or inline of a list a byte was read
1399/// into.
1400fn node_in_items(items: &[ListItem], byte: u32) -> Option<(NodeId, SourceSpan)> {
1401    items.iter().find_map(|item| {
1402        let span = item.span.filter(|span| span.covers(byte))?;
1403        Some(narrowest(
1404            (item.id, span),
1405            node_in_blocks(&item.blocks, byte),
1406        ))
1407    })
1408}
1409
1410/// The span of one node of a list's items, by id.
1411fn span_in_items(items: &[ListItem], node: NodeId) -> Option<SourceSpan> {
1412    items.iter().find_map(|item| {
1413        if item.id == node {
1414            item.span
1415        } else {
1416            span_in_blocks(&item.blocks, node)
1417        }
1418    })
1419}
1420
1421/// The same, over the inlines of one block.
1422fn span_in_inlines(inlines: &[Inline], node: NodeId) -> Option<SourceSpan> {
1423    for inline in inlines {
1424        if inline_id(inline) == node {
1425            return inline_span(inline);
1426        }
1427        let found = match inline {
1428            Inline::Text { .. } | Inline::Code { .. } | Inline::Break { .. } => None,
1429            Inline::Note { blocks, .. } => span_in_blocks(blocks, node),
1430            Inline::Emphasis { children, .. }
1431            | Inline::Strong { children, .. }
1432            | Inline::Link { children, .. }
1433            | Inline::Strikethrough { children, .. }
1434            | Inline::Span { children, .. } => span_in_inlines(children, node),
1435        };
1436        if found.is_some() {
1437            return found;
1438        }
1439    }
1440    None
1441}
1442
1443/// The innermost row, cell, block or inline of a table's rows a byte
1444/// was read into.
1445fn node_in_rows<'a>(
1446    rows: impl Iterator<Item = &'a Row>,
1447    byte: u32,
1448) -> Option<(NodeId, SourceSpan)> {
1449    for row in rows {
1450        let Some(span) = row.span.filter(|span| span.covers(byte)) else {
1451            continue;
1452        };
1453        let inner = row.cells.iter().find_map(|cell| {
1454            let span = cell.span.filter(|span| span.covers(byte))?;
1455            Some(narrowest(
1456                (cell.id, span),
1457                node_in_blocks(&cell.blocks, byte),
1458            ))
1459        });
1460        return Some(narrowest((row.id, span), inner));
1461    }
1462    None
1463}
1464
1465/// The span of one node of a table's rows, by id.
1466fn span_in_rows<'a>(rows: impl Iterator<Item = &'a Row>, node: NodeId) -> Option<SourceSpan> {
1467    for row in rows {
1468        if row.id == node {
1469            return row.span;
1470        }
1471        for cell in &row.cells {
1472            if cell.id == node {
1473                return cell.span;
1474            }
1475            let found = span_in_blocks(&cell.blocks, node);
1476            if found.is_some() {
1477                return found;
1478            }
1479        }
1480    }
1481    None
1482}
1483
1484/// What a sheet names one block by.
1485pub fn block_attributes(block: &Block) -> &Attributes {
1486    match block {
1487        Block::Heading { attributes, .. }
1488        | Block::Paragraph { attributes, .. }
1489        | Block::Blockquote { attributes, .. }
1490        | Block::ThematicBreak { attributes, .. }
1491        | Block::PageBreak { attributes, .. }
1492        | Block::ColumnBreak { attributes, .. }
1493        | Block::Image { attributes, .. }
1494        | Block::List { attributes, .. }
1495        | Block::CodeBlock { attributes, .. }
1496        | Block::Table { attributes, .. } => attributes,
1497    }
1498}
1499
1500/// The same, for one inline.
1501pub fn inline_attributes(inline: &Inline) -> &Attributes {
1502    match inline {
1503        Inline::Text { attributes, .. }
1504        | Inline::Code { attributes, .. }
1505        | Inline::Break { attributes, .. }
1506        | Inline::Emphasis { attributes, .. }
1507        | Inline::Strong { attributes, .. }
1508        | Inline::Link { attributes, .. }
1509        | Inline::Note { attributes, .. }
1510        | Inline::Strikethrough { attributes, .. }
1511        | Inline::Span { attributes, .. } => attributes,
1512    }
1513}
1514
1515/// Where in its source one block was read from.
1516pub fn block_position(block: &Block) -> Option<SourcePos> {
1517    match block {
1518        Block::Heading { position, .. }
1519        | Block::Paragraph { position, .. }
1520        | Block::Blockquote { position, .. }
1521        | Block::ThematicBreak { position, .. }
1522        | Block::PageBreak { position, .. }
1523        | Block::ColumnBreak { position, .. }
1524        | Block::Image { position, .. }
1525        | Block::List { position, .. }
1526        | Block::CodeBlock { position, .. }
1527        | Block::Table { position, .. } => *position,
1528    }
1529}
1530
1531/// The same, for one inline.
1532pub fn inline_position(inline: &Inline) -> Option<SourcePos> {
1533    match inline {
1534        Inline::Text { position, .. }
1535        | Inline::Code { position, .. }
1536        | Inline::Break { position, .. }
1537        | Inline::Emphasis { position, .. }
1538        | Inline::Strong { position, .. }
1539        | Inline::Link { position, .. }
1540        | Inline::Note { position, .. }
1541        | Inline::Strikethrough { position, .. }
1542        | Inline::Span { position, .. } => *position,
1543    }
1544}
1545
1546/// One block's identity.
1547pub fn block_id(block: &Block) -> NodeId {
1548    match block {
1549        Block::Heading { id, .. }
1550        | Block::Paragraph { id, .. }
1551        | Block::Blockquote { id, .. }
1552        | Block::ThematicBreak { id, .. }
1553        | Block::PageBreak { id, .. }
1554        | Block::ColumnBreak { id, .. }
1555        | Block::Image { id, .. }
1556        | Block::List { id, .. }
1557        | Block::CodeBlock { id, .. }
1558        | Block::Table { id, .. } => *id,
1559    }
1560}
1561
1562/// The bytes of its source one block was read from.
1563pub fn block_span(block: &Block) -> Option<SourceSpan> {
1564    match block {
1565        Block::Heading { span, .. }
1566        | Block::Paragraph { span, .. }
1567        | Block::Blockquote { span, .. }
1568        | Block::ThematicBreak { span, .. }
1569        | Block::PageBreak { span, .. }
1570        | Block::ColumnBreak { span, .. }
1571        | Block::Image { span, .. }
1572        | Block::List { span, .. }
1573        | Block::CodeBlock { span, .. }
1574        | Block::Table { span, .. } => *span,
1575    }
1576}
1577
1578/// One inline's identity.
1579pub fn inline_id(inline: &Inline) -> NodeId {
1580    match inline {
1581        Inline::Text { id, .. }
1582        | Inline::Code { id, .. }
1583        | Inline::Break { id, .. }
1584        | Inline::Emphasis { id, .. }
1585        | Inline::Strong { id, .. }
1586        | Inline::Link { id, .. }
1587        | Inline::Note { id, .. }
1588        | Inline::Strikethrough { id, .. }
1589        | Inline::Span { id, .. } => *id,
1590    }
1591}
1592
1593/// The bytes of its source one inline was read from.
1594pub fn inline_span(inline: &Inline) -> Option<SourceSpan> {
1595    match inline {
1596        Inline::Text { span, .. }
1597        | Inline::Code { span, .. }
1598        | Inline::Break { span, .. }
1599        | Inline::Emphasis { span, .. }
1600        | Inline::Strong { span, .. }
1601        | Inline::Link { span, .. }
1602        | Inline::Note { span, .. }
1603        | Inline::Strikethrough { span, .. }
1604        | Inline::Span { span, .. } => *span,
1605    }
1606}
1607
1608fn next_id(next: &mut u32) -> NodeId {
1609    let id = NodeId(*next);
1610    *next += 1;
1611    id
1612}
1613
1614fn assign_block(block: &mut Block, next: &mut u32) {
1615    match block {
1616        Block::Heading { id, inlines, .. } | Block::Paragraph { id, inlines, .. } => {
1617            *id = next_id(next);
1618            for inline in inlines {
1619                assign_inline(inline, next);
1620            }
1621        }
1622        Block::Blockquote { id, blocks, .. } => {
1623            *id = next_id(next);
1624            for nested in blocks {
1625                assign_block(nested, next);
1626            }
1627        }
1628        Block::CodeBlock { id, .. }
1629        | Block::ThematicBreak { id, .. }
1630        | Block::PageBreak { id, .. }
1631        | Block::ColumnBreak { id, .. }
1632        | Block::Image { id, .. } => {
1633            *id = next_id(next);
1634        }
1635        Block::List { id, items, .. } => {
1636            *id = next_id(next);
1637            for item in items {
1638                item.id = next_id(next);
1639                for nested in &mut item.blocks {
1640                    assign_block(nested, next);
1641                }
1642            }
1643        }
1644        Block::Table { id, head, body, .. } => {
1645            *id = next_id(next);
1646            for row in head.iter_mut().chain(body.iter_mut()) {
1647                row.id = next_id(next);
1648                for cell in &mut row.cells {
1649                    cell.id = next_id(next);
1650                    for nested in &mut cell.blocks {
1651                        assign_block(nested, next);
1652                    }
1653                }
1654            }
1655        }
1656    }
1657}
1658
1659fn assign_inline(inline: &mut Inline, next: &mut u32) {
1660    match inline {
1661        Inline::Text { id, .. } | Inline::Code { id, .. } | Inline::Break { id, .. } => {
1662            *id = next_id(next);
1663        }
1664        Inline::Note { id, blocks, .. } => {
1665            *id = next_id(next);
1666            for block in blocks {
1667                assign_block(block, next);
1668            }
1669        }
1670        Inline::Emphasis { id, children, .. }
1671        | Inline::Strong { id, children, .. }
1672        | Inline::Link { id, children, .. }
1673        | Inline::Strikethrough { id, children, .. }
1674        | Inline::Span { id, children, .. } => {
1675            *id = next_id(next);
1676            for child in children {
1677                assign_inline(child, next);
1678            }
1679        }
1680    }
1681}
1682
1683#[cfg(test)]
1684mod tests {
1685    use super::*;
1686
1687    /// Part: `NodeId::element` still maps a pseudo-element id to its
1688    /// element. A renumbered section moves the id with its element,
1689    /// and the id stays one no content node has.
1690    #[test]
1691    fn a_pseudo_element_maps_to_its_element_and_moves_with_it() {
1692        let element = NodeId::new(40);
1693        let kinds = [
1694            PseudoElement::Before,
1695            PseudoElement::After,
1696            PseudoElement::FirstLetter,
1697            PseudoElement::FirstLine,
1698        ];
1699        let ids: Vec<NodeId> = kinds.iter().map(|which| element.pseudo(*which)).collect();
1700        for (id, which) in ids.iter().zip(kinds) {
1701            assert_eq!(id.pseudo_element(), Some((element, which)));
1702            assert_eq!(id.element(), element);
1703            assert!(id.get() >= 1 << 31, "{id:?} is in the range of content ids");
1704            assert_eq!(
1705                id.shifted(-3).pseudo_element(),
1706                Some((NodeId::new(37), which))
1707            );
1708            assert_eq!(ids.iter().filter(|other| *other == id).count(), 1);
1709        }
1710        assert_eq!(element.pseudo_element(), None);
1711        assert_eq!(element.element(), element);
1712        assert_eq!(element.shifted(-3).pseudo_element(), None);
1713    }
1714
1715    /// The markdown the sample tree was read from, so that its spans
1716    /// are the bytes of something rather than numbers made up.
1717    const SOURCE: &str = "\
1718# Chapter One
1719
1720It was the kind of morning that made you suspicious — too *clean*, too quiet.
1721
1722> \"Nobody's early here.\"
1723
1724---
1725
1726![The drawer of knives](images/drawer.png)
1727
17281. A watch
17292. A purse
1730
1731| Pocket | Found |
1732|:---|---:|
1733| Right | A handkerchief |
1734";
1735
1736    /// The bytes of the sample source one stretch of it covers.
1737    fn span(of: &str) -> Option<SourceSpan> {
1738        let start = SOURCE.find(of).expect("the sample source has it");
1739        Some(SourceSpan {
1740            start: start as u32,
1741            end: (start + of.len()) as u32,
1742        })
1743    }
1744
1745    /// Test-local shorthand: an unassigned text run, spanning the
1746    /// bytes of the source it reads as.
1747    fn text(value: &str) -> Inline {
1748        Inline::Text {
1749            id: NodeId::UNASSIGNED,
1750            value: value.into(),
1751            attributes: Attributes::default(),
1752            position: None,
1753            span: span(value),
1754        }
1755    }
1756
1757    /// An item of the sample list: one paragraph of one run, the item
1758    /// spanning the line it was written on.
1759    fn item(line: &str, value: &str) -> ListItem {
1760        ListItem {
1761            blocks: vec![Block::Paragraph {
1762                id: NodeId::UNASSIGNED,
1763                inlines: vec![text(value)],
1764                attributes: Attributes::default(),
1765                position: None,
1766                span: span(value),
1767            }],
1768            span: span(line),
1769            ..ListItem::default()
1770        }
1771    }
1772
1773    /// A list serializes as its items and each item as its blocks. A
1774    /// list that counts from one leaves `start` out.
1775    #[test]
1776    fn a_list_serializes_as_items_of_blocks() {
1777        let list = sample_book().sections[0].blocks[5].clone();
1778        let json = serde_json::to_value(&list).unwrap();
1779        assert_eq!(json["type"], "list");
1780        assert_eq!(json["ordered"], true);
1781        assert_eq!(json["tight"], true);
1782        assert!(json.get("start").is_none(), "{json}");
1783        assert_eq!(
1784            json["items"][1]["blocks"][0]["inlines"][0]["value"],
1785            "A purse"
1786        );
1787
1788        let Block::List {
1789            ordered,
1790            tight,
1791            items,
1792            attributes,
1793            position,
1794            span,
1795            ..
1796        } = list
1797        else {
1798            panic!("the sixth block is a list");
1799        };
1800        let seventh = Block::List {
1801            id: NodeId::UNASSIGNED,
1802            ordered,
1803            start: 7,
1804            tight,
1805            items,
1806            attributes,
1807            position,
1808            span,
1809        };
1810        let json = serde_json::to_value(&seventh).unwrap();
1811        assert_eq!(json["start"], 7);
1812        let read: Block = serde_json::from_value(json).unwrap();
1813        assert_eq!(read, seventh);
1814    }
1815
1816    /// A list is numbered like every other node: the list before its
1817    /// items, an item before what it holds. A byte of an item answers
1818    /// with the run typed into it, and a byte of its marker with the
1819    /// item.
1820    #[test]
1821    fn a_list_numbers_its_items_and_a_byte_of_one_answers_with_its_run() {
1822        let mut book = sample_book();
1823        book.assign_node_ids();
1824        let Block::List { id, items, .. } = &book.sections[0].blocks[5] else {
1825            panic!("the sixth block is a list");
1826        };
1827        assert!(*id < items[0].id);
1828        assert!(items[0].id < block_id(&items[0].blocks[0]));
1829        assert!(block_id(&items[0].blocks[0]) < items[1].id);
1830        let held = book.subtree(*id).expect("the book holds the list");
1831        assert!(held.contains(&block_id(&items[1].blocks[0]).get()));
1832
1833        let Block::Paragraph { inlines, .. } = &items[1].blocks[0] else {
1834            panic!("the item holds a paragraph");
1835        };
1836        let byte = SOURCE.find("purse").unwrap() as u32;
1837        assert_eq!(
1838            book.node_at("chapter-01.md", byte),
1839            Some(inline_id(&inlines[0]))
1840        );
1841        let marker = SOURCE.find("2. A purse").unwrap() as u32;
1842        assert_eq!(book.node_at("chapter-01.md", marker), Some(items[1].id));
1843    }
1844
1845    /// A row of the sample table, spanning the line it was written on.
1846    fn row(line: &str, cells: Vec<Cell>) -> Row {
1847        Row {
1848            cells,
1849            span: span(line),
1850            ..Row::default()
1851        }
1852    }
1853
1854    /// A cell of the sample table: one paragraph of one run, under the
1855    /// alignment its column was written with.
1856    fn cell(value: &str, align: Alignment) -> Cell {
1857        Cell {
1858            blocks: vec![Block::Paragraph {
1859                id: NodeId::UNASSIGNED,
1860                inlines: vec![text(value)],
1861                attributes: Attributes::default(),
1862                position: None,
1863                span: span(value),
1864            }],
1865            align: Some(align),
1866            span: span(value),
1867            ..Cell::default()
1868        }
1869    }
1870
1871    /// The sample book's table.
1872    fn table(book: &Book) -> (&[Row], &[Row]) {
1873        let Some(Block::Table { head, body, .. }) = book.sections[0].blocks.last() else {
1874            panic!("the sample book ends with a table");
1875        };
1876        (head, body)
1877    }
1878
1879    /// A table serializes as its rows, each row as its cells, and each
1880    /// cell as its blocks and the alignment its column was written
1881    /// with.
1882    #[test]
1883    fn a_table_serializes_as_rows_of_cells() {
1884        let json = serde_json::to_value(sample_book().sections[0].blocks.last()).unwrap();
1885        assert_eq!(json["type"], "table");
1886        assert_eq!(json["head"][0]["cells"][1]["align"], "right");
1887        assert_eq!(
1888            json["body"][0]["cells"][1]["blocks"][0],
1889            serde_json::json!({
1890                "type": "paragraph",
1891                "inlines": [{"type": "text", "value": "A handkerchief", "span": {
1892                    "start": SOURCE.find("A handkerchief").unwrap(),
1893                    "end": SOURCE.find("A handkerchief").unwrap() + "A handkerchief".len(),
1894                }}],
1895                "span": {
1896                    "start": SOURCE.find("A handkerchief").unwrap(),
1897                    "end": SOURCE.find("A handkerchief").unwrap() + "A handkerchief".len(),
1898                },
1899            }),
1900        );
1901    }
1902
1903    /// A table is numbered like every other node: the table before its
1904    /// rows, a row before its cells, a cell before what it holds, and
1905    /// the header rows before the body rows.
1906    #[test]
1907    fn a_table_numbers_its_rows_and_cells_in_reading_order() {
1908        let mut book = sample_book();
1909        book.assign_node_ids();
1910        let table_id = block_id(book.sections[0].blocks.last().unwrap());
1911        let (head, body) = table(&book);
1912        let header = &head[0];
1913        let first = &body[0];
1914        assert!(table_id < header.id);
1915        assert!(header.id < header.cells[0].id);
1916        assert!(header.cells[0].id < block_id(&header.cells[0].blocks[0]));
1917        assert!(block_id(&header.cells[1].blocks[0]) < first.id);
1918        assert!(first.id < first.cells[1].id);
1919
1920        // A row covers its cells, and a cell what is in it.
1921        let held = book.subtree(first.id).expect("the book holds the row");
1922        assert_eq!(held.start, first.id.get());
1923        assert!(held.contains(&block_id(&first.cells[1].blocks[0]).get()));
1924        assert_eq!(
1925            book.subtree(table_id).map(|held| held.end),
1926            book.subtree(book.sections[0].id).map(|held| held.end),
1927            "the table is the last node of the book",
1928        );
1929    }
1930
1931    /// A byte of a cell answers with the run typed into it, and a
1932    /// byte of the pipes between two cells answers with the row.
1933    #[test]
1934    fn a_byte_of_a_cell_answers_with_the_run_written_there() {
1935        let mut book = sample_book();
1936        book.assign_node_ids();
1937        let (_, body) = table(&book);
1938        let Block::Paragraph { inlines, .. } = &body[0].cells[1].blocks[0] else {
1939            panic!("the cell holds a paragraph");
1940        };
1941        let byte = SOURCE.find("handkerchief").unwrap() as u32;
1942        assert_eq!(
1943            book.node_at("chapter-01.md", byte),
1944            Some(inline_id(&inlines[0]))
1945        );
1946        let pipe = SOURCE.find("| Right").unwrap() as u32;
1947        assert_eq!(book.node_at("chapter-01.md", pipe), Some(body[0].id));
1948    }
1949
1950    /// What the engine writes, a host may hand back. The fields
1951    /// left out of the JSON when they are empty are the ones this
1952    /// catches: a book with no extras serializes without the map it
1953    /// then has to be readable without.
1954    #[test]
1955    fn a_tree_the_engine_wrote_reads_back() {
1956        for book in [sample_book(), Book::default()] {
1957            let json = serde_json::to_string(&book).unwrap();
1958            let read: Book = serde_json::from_str(&json).unwrap();
1959            assert_eq!(read, book, "{json}");
1960        }
1961    }
1962
1963    fn sample_book() -> Book {
1964        Book {
1965            metadata: Metadata {
1966                title: Some("The Fixture Book".into()),
1967                author: Some("A. Author".into()),
1968                extra: [("language".to_string(), "en".to_string())]
1969                    .into_iter()
1970                    .collect(),
1971            },
1972            sections: vec![Section {
1973                attributes: Default::default(),
1974                id: NodeId::UNASSIGNED,
1975                source: Some("chapter-01.md".into()),
1976                title: Some("Chapter One".into()),
1977                blocks: vec![
1978                    Block::Heading {
1979                        id: NodeId::UNASSIGNED,
1980                        level: HeadingLevel::H1,
1981                        inlines: vec![text("Chapter One")],
1982                        attributes: Attributes::default(),
1983                        position: Some(SourcePos { line: 1, column: 1 }),
1984                        span: span("# Chapter One\n"),
1985                    },
1986                    Block::Paragraph {
1987                        id: NodeId::UNASSIGNED,
1988                        inlines: vec![
1989                            text("It was the kind of morning that made you suspicious — too "),
1990                            Inline::Emphasis {
1991                                id: NodeId::UNASSIGNED,
1992                                children: vec![text("clean")],
1993                                attributes: Attributes::default(),
1994                                position: None,
1995                                span: span("*clean*"),
1996                            },
1997                            text(", too quiet."),
1998                        ],
1999                        attributes: Attributes::default(),
2000                        position: Some(SourcePos { line: 3, column: 1 }),
2001                        span: span(
2002                            "It was the kind of morning that made you suspicious — too *clean*, too quiet.\n",
2003                        ),
2004                    },
2005                    Block::Blockquote {
2006                        id: NodeId::UNASSIGNED,
2007                        blocks: vec![Block::Paragraph {
2008                            id: NodeId::UNASSIGNED,
2009                            inlines: vec![text("\"Nobody's early here.\"")],
2010                            attributes: Attributes::default(),
2011                            position: None,
2012                            span: span("\"Nobody's early here.\"\n"),
2013                        }],
2014                        attributes: Attributes::default(),
2015                        position: Some(SourcePos { line: 5, column: 1 }),
2016                        span: span("> \"Nobody's early here.\"\n"),
2017                    },
2018                    Block::ThematicBreak {
2019                        id: NodeId::UNASSIGNED,
2020                        attributes: Attributes::default(),
2021                        position: Some(SourcePos { line: 7, column: 1 }),
2022                        span: span("---\n"),
2023                    },
2024                    Block::Image {
2025                        id: NodeId::UNASSIGNED,
2026                        url: "images/drawer.png".into(),
2027                        alt: "The drawer of knives".into(),
2028                        attributes: Attributes::default(),
2029                        position: Some(SourcePos { line: 9, column: 1 }),
2030                        span: span("![The drawer of knives](images/drawer.png)"),
2031                    },
2032                    Block::List {
2033                        id: NodeId::UNASSIGNED,
2034                        ordered: true,
2035                        start: 1,
2036                        tight: true,
2037                        items: vec![
2038                            item("1. A watch\n", "A watch"),
2039                            item("2. A purse\n", "A purse"),
2040                        ],
2041                        attributes: Attributes::default(),
2042                        position: Some(SourcePos {
2043                            line: 11,
2044                            column: 1,
2045                        }),
2046                        span: span("1. A watch\n2. A purse\n"),
2047                    },
2048                    Block::Table {
2049                        id: NodeId::UNASSIGNED,
2050                        head: vec![row(
2051                            "| Pocket | Found |\n",
2052                            vec![
2053                                cell("Pocket", Alignment::Left),
2054                                cell("Found", Alignment::Right),
2055                            ],
2056                        )],
2057                        body: vec![row(
2058                            "| Right | A handkerchief |\n",
2059                            vec![
2060                                cell("Right", Alignment::Left),
2061                                cell("A handkerchief", Alignment::Right),
2062                            ],
2063                        )],
2064                        attributes: Attributes::default(),
2065                        position: Some(SourcePos {
2066                            line: 14,
2067                            column: 1,
2068                        }),
2069                        span: span("| Pocket | Found |\n|:---|---:|\n| Right | A handkerchief |\n"),
2070                    },
2071                ],
2072                position: Some(SourcePos { line: 1, column: 1 }),
2073                span: Some(SourceSpan {
2074                    start: 0,
2075                    end: SOURCE.len() as u32,
2076                }),
2077            }],
2078        }
2079    }
2080
2081    /// Every id in the tree, in walk order — the order assignment uses.
2082    fn collect_ids(book: &Book) -> Vec<NodeId> {
2083        fn walk_block(ids: &mut Vec<NodeId>, block: &Block) {
2084            match block {
2085                Block::Heading { id, inlines, .. } | Block::Paragraph { id, inlines, .. } => {
2086                    ids.push(*id);
2087                    ids.extend(inlines.iter().flat_map(walk_inline_ids));
2088                }
2089                Block::Blockquote { id, blocks, .. } => {
2090                    ids.push(*id);
2091                    for nested in blocks {
2092                        walk_block(ids, nested);
2093                    }
2094                }
2095                Block::CodeBlock { id, .. }
2096                | Block::ThematicBreak { id, .. }
2097                | Block::PageBreak { id, .. }
2098                | Block::ColumnBreak { id, .. }
2099                | Block::Image { id, .. } => ids.push(*id),
2100                Block::List { id, items, .. } => {
2101                    ids.push(*id);
2102                    for item in items {
2103                        ids.push(item.id);
2104                        for nested in &item.blocks {
2105                            walk_block(ids, nested);
2106                        }
2107                    }
2108                }
2109                Block::Table { id, head, body, .. } => {
2110                    ids.push(*id);
2111                    for row in rows(head, body) {
2112                        ids.push(row.id);
2113                        for cell in &row.cells {
2114                            ids.push(cell.id);
2115                            for nested in &cell.blocks {
2116                                walk_block(ids, nested);
2117                            }
2118                        }
2119                    }
2120                }
2121            }
2122        }
2123
2124        fn walk_inline_ids(inline: &Inline) -> Vec<NodeId> {
2125            match inline {
2126                Inline::Text { id, .. } | Inline::Code { id, .. } | Inline::Break { id, .. } => {
2127                    vec![*id]
2128                }
2129                Inline::Note { id, blocks, .. } => {
2130                    let mut ids = vec![*id];
2131                    for block in blocks {
2132                        walk_block(&mut ids, block);
2133                    }
2134                    ids
2135                }
2136                Inline::Emphasis { id, children, .. }
2137                | Inline::Strong { id, children, .. }
2138                | Inline::Link { id, children, .. }
2139                | Inline::Strikethrough { id, children, .. }
2140                | Inline::Span { id, children, .. } => {
2141                    let mut ids = vec![*id];
2142                    ids.extend(children.iter().flat_map(walk_inline_ids));
2143                    ids
2144                }
2145            }
2146        }
2147
2148        let mut ids = Vec::new();
2149        for section in &book.sections {
2150            ids.push(section.id);
2151            for block in &section.blocks {
2152                walk_block(&mut ids, block);
2153            }
2154        }
2155        ids
2156    }
2157
2158    /// Two serializations of one tree are the same bytes: a dump is
2159    /// something to diff.
2160    #[test]
2161    fn serialization_is_stable() {
2162        let mut book = sample_book();
2163        book.assign_node_ids();
2164        let once = serde_json::to_string_pretty(&book).unwrap();
2165        assert_eq!(once, serde_json::to_string_pretty(&book).unwrap());
2166    }
2167
2168    /// A serialized tree has no ids: the tree is authoritative,
2169    /// and identity is the engine's to hand out.
2170    #[test]
2171    fn ids_are_never_serialized() {
2172        let mut book = sample_book();
2173        book.assign_node_ids();
2174        let json = serde_json::to_string(&book).unwrap();
2175        assert!(!json.contains("\"id\""));
2176    }
2177
2178    /// Tags are `type`, text runs are plain strings, and the shape
2179    /// maps onto mdast.
2180    #[test]
2181    fn a_serialized_tree_is_internally_tagged() {
2182        let block = Block::Paragraph {
2183            id: NodeId::UNASSIGNED,
2184            inlines: vec![
2185                Inline::Text {
2186                    id: NodeId::UNASSIGNED,
2187                    value: "plain ".into(),
2188                    attributes: Attributes::default(),
2189                    position: None,
2190                    span: None,
2191                },
2192                Inline::Strong {
2193                    id: NodeId::UNASSIGNED,
2194                    children: vec![Inline::Text {
2195                        id: NodeId::UNASSIGNED,
2196                        value: "bold".into(),
2197                        attributes: Attributes::default(),
2198                        position: None,
2199                        span: None,
2200                    }],
2201                    attributes: Attributes::default(),
2202                    position: None,
2203                    span: None,
2204                },
2205            ],
2206            attributes: Attributes::default(),
2207            position: Some(SourcePos { line: 4, column: 1 }),
2208            span: Some(SourceSpan { start: 40, end: 58 }),
2209        };
2210        assert_eq!(
2211            serde_json::to_value(&block).unwrap(),
2212            serde_json::json!({
2213                "type": "paragraph",
2214                "inlines": [
2215                    {"type": "text", "value": "plain "},
2216                    {"type": "strong", "children": [{"type": "text", "value": "bold"}]},
2217                ],
2218                "position": {"line": 4, "column": 1},
2219                "span": {"start": 40, "end": 58},
2220            }),
2221        );
2222    }
2223
2224    /// A hard break is a node of its own, with no text in it. It
2225    /// serializes as its tag, reads back, and is a newline in the
2226    /// text of the inlines around it.
2227    #[test]
2228    fn a_hard_break_is_an_inline_of_its_own() {
2229        let words = |value: &str| Inline::Text {
2230            id: NodeId::UNASSIGNED,
2231            value: value.into(),
2232            attributes: Attributes::default(),
2233            position: None,
2234            span: None,
2235        };
2236        let inlines = vec![
2237            words("Chapter One"),
2238            Inline::Break {
2239                id: NodeId::UNASSIGNED,
2240                attributes: Attributes::default(),
2241                position: None,
2242                span: None,
2243            },
2244            words("The Voyage to Lilliput"),
2245        ];
2246        let json = serde_json::to_value(&inlines).unwrap();
2247        assert_eq!(json[1], serde_json::json!({"type": "break"}));
2248        let read: Vec<Inline> = serde_json::from_value(json).unwrap();
2249        assert_eq!(read, inlines);
2250        assert_eq!(super::text(&inlines), "Chapter One\nThe Voyage to Lilliput");
2251    }
2252
2253    /// A heading level is 1-6, and a level outside that is rejected
2254    /// rather than clamped.
2255    #[test]
2256    fn heading_levels_run_one_to_six() {
2257        for level in 1..=6u8 {
2258            let heading = HeadingLevel::try_from(level).expect("1-6 is a heading level");
2259            assert_eq!(u8::from(heading), level);
2260        }
2261        for outside in [0u8, 7, 255] {
2262            assert_eq!(
2263                HeadingLevel::try_from(outside),
2264                Err(InvalidHeadingLevel(outside)),
2265            );
2266        }
2267    }
2268
2269    /// Ids are dense (exactly `1..=n`), assigned pre-order, and the
2270    /// same on every assignment.
2271    #[test]
2272    fn node_ids_are_dense_pre_order_and_deterministic() {
2273        let mut book = sample_book();
2274        book.assign_node_ids();
2275        let ids = collect_ids(&book);
2276
2277        // Dense from 1: same length, same set, no gaps.
2278        let mut sorted = ids.clone();
2279        sorted.sort_by_key(|id| id.get());
2280        sorted.dedup();
2281        assert_eq!(sorted.len(), ids.len());
2282        assert_eq!(sorted.first().unwrap().get(), 1);
2283        assert_eq!(sorted.last().unwrap().get(), ids.len() as u32);
2284
2285        // Pre-order: the section precedes its first block, a block
2286        // precedes its first inline, an inline precedes its children.
2287        let section = book.sections[0].id;
2288        let Block::Heading { id: heading, .. } = &book.sections[0].blocks[0] else {
2289            panic!("fixture starts with a heading");
2290        };
2291        let Block::Paragraph {
2292            id: paragraph,
2293            inlines,
2294            ..
2295        } = &book.sections[0].blocks[1]
2296        else {
2297            panic!("second block is a paragraph");
2298        };
2299        assert!(section.get() < heading.get());
2300        assert!(heading.get() < paragraph.get());
2301        let Inline::Emphasis {
2302            id: emphasis,
2303            children,
2304            ..
2305        } = &inlines[1]
2306        else {
2307            panic!("second inline is emphasis");
2308        };
2309        let Inline::Text { id: child, .. } = &children[0] else {
2310            panic!("emphasis child is text");
2311        };
2312        assert!(paragraph.get() < emphasis.get());
2313        assert!(emphasis.get() < child.get());
2314
2315        // Deterministic: a fresh assignment over the same tree is
2316        // byte-identical.
2317        let first = collect_ids(&book);
2318        book.assign_node_ids();
2319        assert_eq!(first, collect_ids(&book));
2320    }
2321
2322    /// A byte of the source answers with the node written there, and
2323    /// the innermost one: the run inside the emphasis rather than the
2324    /// emphasis, the emphasis rather than the paragraph.
2325    #[test]
2326    fn a_byte_answers_with_the_innermost_node_written_there() {
2327        let mut book = sample_book();
2328        book.assign_node_ids();
2329
2330        let letter = SOURCE.find("clean").unwrap() as u32;
2331        let Some(Inline::Emphasis {
2332            id: emphasis,
2333            children,
2334            ..
2335        }) = paragraph(&book).get(1)
2336        else {
2337            panic!("the sample paragraph holds an emphasis");
2338        };
2339        assert_eq!(
2340            book.node_at("chapter-01.md", letter),
2341            Some(inline_id(&children[0]))
2342        );
2343        // The asterisk is markup, which no run was shaped from, so it
2344        // answers with the construct it opens.
2345        assert_eq!(book.node_at("chapter-01.md", letter - 1), Some(*emphasis));
2346        // A byte between two blocks belongs to no node under the
2347        // section, and the section is what covers it.
2348        let blank = SOURCE.find("\n\n").unwrap() as u32 + 1;
2349        assert_eq!(
2350            book.node_at("chapter-01.md", blank),
2351            Some(book.sections[0].id)
2352        );
2353        assert_eq!(book.node_at("chapter-01.md", SOURCE.len() as u32), None);
2354    }
2355
2356    /// A question about one source is answered by that source's
2357    /// nodes: the same byte of two files is two different nodes, and
2358    /// a file the book has never read answers with nothing.
2359    #[test]
2360    fn a_question_is_answered_from_one_source() {
2361        let mut book = sample_book();
2362        let mut second = book.sections[0].clone();
2363        second.source = Some("chapter-02.md".into());
2364        book.sections.push(second);
2365        book.assign_node_ids();
2366
2367        let letter = SOURCE.find("clean").unwrap() as u32;
2368        let first = book
2369            .node_at("chapter-01.md", letter)
2370            .expect("the first chapter");
2371        let second = book.node_at("chapter-02.md", letter).expect("the second");
2372        assert_ne!(first, second);
2373        assert_eq!(
2374            book.source_of(first).map(|(name, _)| name),
2375            Some("chapter-01.md")
2376        );
2377        assert_eq!(
2378            book.source_of(second).map(|(name, _)| name),
2379            Some("chapter-02.md")
2380        );
2381        assert_eq!(book.node_at("chapter-03.md", letter), None);
2382    }
2383
2384    /// Every node of a parsed tree says where it was read from, and
2385    /// the byte it starts at answers with that node again.
2386    #[test]
2387    fn a_node_taken_to_its_source_and_back_is_the_same_node() {
2388        let mut book = sample_book();
2389        book.assign_node_ids();
2390
2391        for id in collect_ids(&book) {
2392            let (source, span) = book
2393                .source_of(id)
2394                .expect("a parsed node was read from a file");
2395            assert_eq!(source, "chapter-01.md");
2396            assert!(
2397                span.end as usize <= SOURCE.len(),
2398                "node {} covers {span:?}, which is past the source",
2399                id.get(),
2400            );
2401            let there = book
2402                .node_at(source, span.start)
2403                .expect("a node was read there");
2404            let (_, back) = book.source_of(there).expect("and it says so");
2405            assert_eq!(back.start, span.start, "node {} starts elsewhere", id.get());
2406        }
2407    }
2408
2409    /// A tree built rather than parsed was read from nothing, and
2410    /// both questions say so rather than guessing.
2411    #[test]
2412    fn a_tree_built_rather_than_parsed_answers_with_nothing() {
2413        let mut book = Book {
2414            metadata: Metadata::default(),
2415            sections: vec![Section {
2416                attributes: Default::default(),
2417                source: Some("chapter-01.md".into()),
2418                blocks: vec![Block::Paragraph {
2419                    id: NodeId::UNASSIGNED,
2420                    inlines: vec![Inline::Text {
2421                        id: NodeId::UNASSIGNED,
2422                        value: "Built by hand.".into(),
2423                        attributes: Attributes::default(),
2424                        position: None,
2425                        span: None,
2426                    }],
2427                    attributes: Attributes::default(),
2428                    position: None,
2429                    span: None,
2430                }],
2431                ..Section::default()
2432            }],
2433        };
2434        book.assign_node_ids();
2435
2436        assert_eq!(book.node_at("chapter-01.md", 0), None);
2437        for id in collect_ids(&book) {
2438            assert_eq!(
2439                book.source_of(id),
2440                None,
2441                "node {} was read from nothing",
2442                id.get()
2443            );
2444        }
2445        assert_eq!(book.source_of(NodeId::UNASSIGNED), None);
2446    }
2447
2448    /// A node covers itself and everything under it, as one run of
2449    /// numbers: the section covers the book, the blockquote covers
2450    /// the paragraph quoted in it, and a text run covers only
2451    /// itself.
2452    #[test]
2453    fn a_subtree_is_a_run_of_ids() {
2454        let mut book = sample_book();
2455        book.assign_node_ids();
2456        let ids = collect_ids(&book);
2457        let last = ids.last().expect("the sample book has nodes").get();
2458
2459        let section = book.sections[0].id;
2460        assert_eq!(book.subtree(section), Some(section.get()..last + 1));
2461
2462        let quote = book.sections[0].blocks[2].clone();
2463        let Block::Blockquote { id, blocks, .. } = &quote else {
2464            panic!("the third block is a blockquote");
2465        };
2466        let quoted = block_id(&blocks[0]);
2467        assert_eq!(book.subtree(*id), Some(id.get()..quoted.get() + 2));
2468
2469        for id in ids {
2470            let held = book.subtree(id).expect("the book holds it");
2471            assert_eq!(held.start, id.get(), "a node opens its own run");
2472            assert!(held.end <= last + 1, "a run stops inside the book");
2473        }
2474    }
2475
2476    /// An id the book has no node for is covered by nothing rather
2477    /// than by whatever was numbered near it.
2478    #[test]
2479    fn an_id_the_book_does_not_hold_covers_nothing() {
2480        let mut book = sample_book();
2481        book.assign_node_ids();
2482        let past = collect_ids(&book).last().expect("nodes").get() + 1;
2483
2484        assert_eq!(book.subtree(NodeId::UNASSIGNED), None);
2485        assert_eq!(book.subtree(NodeId::new(past)), None);
2486        assert_eq!(Book::default().subtree(NodeId::new(1)), None);
2487    }
2488
2489    /// The sample book's one paragraph of prose.
2490    fn paragraph(book: &Book) -> &[Inline] {
2491        let Block::Paragraph { inlines, .. } = &book.sections[0].blocks[1] else {
2492            panic!("the second block is a paragraph");
2493        };
2494        inlines
2495    }
2496
2497    /// Acceptance: a page break and a column break serialize under
2498    /// tags of their own.
2499    #[test]
2500    fn a_break_serializes_under_its_own_tag() {
2501        let page = Block::PageBreak {
2502            id: NodeId::UNASSIGNED,
2503            attributes: Attributes::default(),
2504            position: None,
2505            span: None,
2506        };
2507        let column = Block::ColumnBreak {
2508            id: NodeId::UNASSIGNED,
2509            attributes: Attributes::default(),
2510            position: None,
2511            span: None,
2512        };
2513        assert_eq!(
2514            serde_json::to_value(&page).expect("a break serializes"),
2515            serde_json::json!({ "type": "page_break" })
2516        );
2517        assert_eq!(
2518            serde_json::to_value(&column).expect("a break serializes"),
2519            serde_json::json!({ "type": "column_break" })
2520        );
2521    }
2522
2523    /// File + position in all four presence combinations.
2524    #[test]
2525    fn origin_formats_file_line_column() {
2526        let pos = SourcePos {
2527            line: 12,
2528            column: 3,
2529        };
2530        assert_eq!(
2531            origin(Some("chapter-01.md"), Some(pos)),
2532            "chapter-01.md:12:3"
2533        );
2534        assert_eq!(origin(Some("chapter-01.md"), None), "chapter-01.md");
2535        assert_eq!(origin(None, Some(pos)), "12:3");
2536        assert_eq!(origin(None, None), "");
2537    }
2538
2539    /// The names a book reports are the ones its blocks and inlines
2540    /// carry, down to a list item, a table cell and a note, each once
2541    /// and sorted. A section's own names are left out, and a source
2542    /// answers for its own sections alone.
2543    #[test]
2544    fn a_source_reports_the_names_its_blocks_and_inlines_carry() {
2545        let text = |value: &str| serde_json::json!({"type": "text", "value": value});
2546        let book: Book = serde_json::from_value(serde_json::json!({
2547            "metadata": {},
2548            "sections": [
2549                {
2550                    "source": "one.md",
2551                    "attributes": {"id": "front", "classes": ["chapter"]},
2552                    "blocks": [
2553                        {"type": "heading", "level": 1, "inlines": [text("One")],
2554                         "attributes": {"id": "ch1", "classes": ["opening"]}},
2555                        {"type": "paragraph", "inlines": [
2556                            {"type": "span", "children": [text("aside")],
2557                             "attributes": {"classes": ["smallcaps"]}},
2558                            {"type": "note", "blocks": [
2559                                {"type": "paragraph", "inlines": [text("n")],
2560                                 "attributes": {"classes": ["gloss"]}}
2561                            ]}
2562                        ]},
2563                        {"type": "list", "items": [
2564                            {"blocks": [], "attributes": {"id": "first-item", "classes": ["opening"]}}
2565                        ]},
2566                        {"type": "table", "body": [
2567                            {"cells": [{"blocks": [], "attributes": {"classes": ["total"]}}],
2568                             "attributes": {"classes": ["sum"]}}
2569                        ]}
2570                    ]
2571                },
2572                {
2573                    "source": "two.md",
2574                    "blocks": [
2575                        {"type": "blockquote", "blocks": [], "attributes": {"classes": ["epigraph"]}}
2576                    ]
2577                }
2578            ]
2579        }))
2580        .unwrap();
2581
2582        let one = book.names(Some("one.md"));
2583        assert_eq!(
2584            one.classes,
2585            ["gloss", "opening", "smallcaps", "sum", "total"]
2586        );
2587        assert_eq!(one.ids, ["ch1", "first-item"]);
2588        assert_eq!(book.names(Some("two.md")).classes, ["epigraph"]);
2589        assert_eq!(book.names(Some("three.md")), Names::default());
2590        let all = book.names(None);
2591        assert_eq!(
2592            all.classes,
2593            ["epigraph", "gloss", "opening", "smallcaps", "sum", "total"]
2594        );
2595        assert_eq!(all.ids, ["ch1", "first-item"]);
2596    }
2597}