Skip to main content

fleuron/layout/
build.rs

1//! Blocks in, fragments out: margins folded together, breaks
2//! settled, lines broken, the initial letter set beside them.
3
4use std::borrow::Cow;
5use std::sync::Arc;
6
7use crate::content::{
8    Block, Inline, NodeId, PseudoElement, Section, SourcePos, block_attributes, block_id,
9    block_position, inline_attributes, inline_id, notes_in_inlines, origin, text,
10};
11use crate::lines::{Line, LineBreakOptions, Measure, Opening, Patterns, Shaped, Span};
12use crate::pages::{DrawItem, PageBox};
13use crate::style::{
14    Break, ColumnSpan, ComputedStyle, Content, Hyphens, Position, StringPiece, StyleTree,
15    TextAlign, TextJustify,
16};
17
18use super::Paginator;
19use super::cap::Cap;
20use super::flow::Painted;
21use super::fragment::{
22    BreakPoint, Decoration, Decorations, DropCap, Fragment, Marks, Piece, decoration,
23};
24use super::image::ImageSize;
25use super::note::Note;
26use super::reference::Referring;
27
28impl Paginator<'_> {
29    /// One section's blocks as fragments, in document order:
30    /// everything measurement decides, and nothing pagination does.
31    ///
32    /// A block of the section that spans the columns breaks to the
33    /// whole content box, and one that does not breaks to a column. A
34    /// spanning block moves to the next page whole rather than split
35    /// under the columns above it.
36    pub fn section_fragments(&self, section: &Section) -> Vec<Fragment> {
37        let geometry = self.styles.default_page().geometry;
38        let measure = geometry.measure();
39        let mut builder = Builder::new(self, section.source.as_deref());
40        let style = self.styles.style(section.id).clone();
41        builder.name(section.id);
42        let start = builder.open(section.id, &style, &[], 0.0, measure);
43        let column = style.content_box(0.0, measure);
44        let whole = style.content_box(0.0, geometry.content_size().0);
45        for child in children(self.styles, section.id, &section.blocks, section.position) {
46            builder.spanning = self.styles.style(child.id()).column_span == ColumnSpan::All;
47            let (x, measure) = if builder.spanning { whole } else { column };
48            let first = builder.fragments.len();
49            builder.blocks([child], x, measure);
50            if builder.spanning {
51                for fragment in builder.fragments[first..]
52                    .iter_mut()
53                    .filter(|fragment| !matches!(fragment.piece, Piece::Anchor(_)))
54                    .skip(1)
55                {
56                    fragment.break_before = BreakPoint::Forbidden;
57                }
58            }
59        }
60        builder.spanning = false;
61        builder.close(&style, start);
62        builder.fragments
63    }
64}
65
66/// Builds one section's fragments: blocks in, everything the flow
67/// needs to place them out.
68pub(super) struct Builder<'a, 'p> {
69    pub(super) paginator: &'p Paginator<'a>,
70    /// The file the section was read from, for diagnostics.
71    pub(super) source: Option<&'p str>,
72    pub(super) fragments: Vec<Fragment>,
73    /// What the cascade has asked for above the next fragment.
74    pub(super) pending: BreakPoint,
75    /// The collapsible margin standing above the next fragment,
76    /// which is the larger of the margins that met there.
77    pub(super) margin: f32,
78    /// Space above the next fragment that no margin collapses
79    /// through: the borders and padding the blocks around it set.
80    pub(super) fixed: f32,
81    /// What the blocks opened so far have set, waiting for a fragment
82    /// to attach it to a page.
83    pub(super) pending_marks: Option<Box<Marks>>,
84    /// The blocks still open, outermost first.
85    open: Vec<Pending>,
86    /// Whether the block being built spans every column.
87    spanning: bool,
88    /// The layer the block being built paints in. Every block opens
89    /// before it emits anything, so opening one is where this is
90    /// settled.
91    pub(super) layer: i32,
92    /// The sum of the moves of every relative block still open around
93    /// the block being built.
94    offset: (f32, f32),
95    /// The value of `offset` before each open relative block added its
96    /// move, innermost last.
97    moved: Vec<(f32, f32)>,
98    /// How much shows of what the block being built paints, from 0 to
99    /// 1: the `opacity` of every block still open around it, its own
100    /// included, multiplied together.
101    pub(super) opacity: f32,
102    /// The value of `opacity` before each open block multiplied its
103    /// own into it, innermost last.
104    faded: Vec<f32>,
105    /// The block this builder lays out on its own, against the page.
106    /// Its own `position: absolute` does not anchor it a second time.
107    pub(super) lifted: Option<NodeId>,
108    /// How far down the fragments emitted so far reach, space above
109    /// each one included.
110    depth: f32,
111    /// The blocks still open that ask for a height, innermost last.
112    tall: Vec<Tall>,
113}
114
115impl<'a, 'p> Builder<'a, 'p> {
116    /// A builder with nothing built yet, over blocks read from
117    /// `source`.
118    pub(super) fn new(paginator: &'p Paginator<'a>, source: Option<&'p str>) -> Builder<'a, 'p> {
119        Builder {
120            paginator,
121            source,
122            fragments: Vec::new(),
123            pending: BreakPoint::Allowed,
124            margin: 0.0,
125            fixed: 0.0,
126            pending_marks: None,
127            open: Vec::new(),
128            spanning: false,
129            layer: 0,
130            offset: (0.0, 0.0),
131            moved: Vec::new(),
132            opacity: 1.0,
133            faded: Vec::new(),
134            lifted: None,
135            depth: 0.0,
136            tall: Vec::new(),
137        }
138    }
139}
140
141/// One box among the children of an element: a block the manuscript
142/// wrote, or a box that `::before` or `::after` generates.
143#[derive(Debug, Clone, Copy)]
144pub(super) enum Child<'b> {
145    Block(&'b Block),
146    /// A generated box, and where its element was written.
147    Generated(NodeId, Option<SourcePos>),
148}
149
150impl Child<'_> {
151    pub(super) fn id(self) -> NodeId {
152        match self {
153            Child::Block(block) => block_id(block),
154            Child::Generated(id, _) => id,
155        }
156    }
157}
158
159/// The children of the element `id`, written at `position`: the box
160/// `::before` generates, then `blocks`, then the box `::after`
161/// generates.
162pub(super) fn children<'b>(
163    styles: &StyleTree,
164    id: NodeId,
165    blocks: &'b [Block],
166    position: Option<SourcePos>,
167) -> impl Iterator<Item = Child<'b>> + use<'b> {
168    let generated = |which| {
169        styles
170            .pseudo_element(id, which)
171            .map(|id| Child::Generated(id, position))
172    };
173    generated(PseudoElement::Before)
174        .into_iter()
175        .chain(blocks.iter().map(Child::Block))
176        .chain(generated(PseudoElement::After))
177}
178
179/// A block that asks for a height, while its fragments are still being
180/// built.
181struct Tall {
182    /// Where its content box starts, as `depth` counts.
183    top: f32,
184    /// The height `height` gives its content box, which is what a
185    /// percentage inside it measures against. `None` for `auto`.
186    definite: Option<f32>,
187    /// The least height its content box takes.
188    least: f32,
189}
190
191/// A block while its fragments are still being built.
192struct Pending {
193    /// The fragment its first one will be.
194    start: usize,
195    /// What `fixed` stood at when the block's border box opened, and
196    /// what the distance down to its first fragment is measured from.
197    open_fixed: f32,
198    /// Whether anything has been committed above the block's first
199    /// fragment yet. Until something has, a margin committed inside
200    /// the block is one that collapsed through it, and sits above its
201    /// border box rather than inside it.
202    started: bool,
203    decoration: Decoration,
204}
205
206impl Builder<'_, '_> {
207    fn styles(&self) -> &StyleTree {
208        self.paginator.styles
209    }
210
211    /// Folds one `break-before` or `break-after` into what is already
212    /// asked above the next fragment. A forced break outranks an
213    /// avoided one, and either outranks `auto`. A page break outranks
214    /// a column break, being the same break carried further, and a
215    /// break to one side outranks both.
216    pub(super) fn ask(&mut self, wanted: Break) {
217        self.pending = match (self.pending, wanted) {
218            (BreakPoint::Forced(Break::Column), Break::Page)
219            | (BreakPoint::Forced(Break::Column | Break::Page), Break::Side(_)) => {
220                BreakPoint::Forced(wanted)
221            }
222            (BreakPoint::Forced(forced), _) => BreakPoint::Forced(forced),
223            (_, Break::Page) => BreakPoint::Forced(Break::Page),
224            (_, Break::Side(side)) => BreakPoint::Forced(Break::Side(side)),
225            (_, Break::Column) => BreakPoint::Forced(Break::Column),
226            (_, Break::Avoid) => BreakPoint::Forbidden,
227            (pending, Break::Auto) => pending,
228        };
229    }
230
231    /// Opens a block: what it asks for above itself, what it sets for
232    /// the page furniture, the space its top margin leaves, and the
233    /// decoration it paints across `x` and `measure`.
234    ///
235    /// Adjacent margins collapse to the larger. A top border or
236    /// padding is not a margin: it takes height of its own, and the
237    /// margins on either side of it no longer meet.
238    pub(super) fn open(
239        &mut self,
240        node: NodeId,
241        style: &ComputedStyle,
242        inlines: &[Inline],
243        x: f32,
244        measure: f32,
245    ) -> usize {
246        self.ask(style.break_before);
247        self.mark(style, inlines);
248        self.layer = style.z_index;
249        if style.position == Position::Relative {
250            // A percentage is a percentage of the page area that the
251            // lines of the section break to.
252            let area = self.styles().default_page().geometry.content_size();
253            let (dx, dy) = style.inset.offset(area);
254            self.moved.push(self.offset);
255            self.offset = (self.offset.0 + dx, self.offset.1 + dy);
256        }
257        self.faded.push(self.opacity);
258        self.opacity *= style.opacity;
259        self.margin = self.margin.max(style.margin.top);
260        let start = self.fragments.len();
261        let border = style.border.widths();
262        let backdrop = self.paginator.backdrop(&style.background);
263        self.open.push(Pending {
264            start,
265            open_fixed: self.fixed,
266            started: false,
267            decoration: decoration(
268                node,
269                style,
270                x,
271                measure,
272                backdrop,
273                (self.offset, self.opacity),
274            ),
275        });
276        if border.top + style.padding.top > 0.0 {
277            let margin = std::mem::take(&mut self.margin);
278            self.commit(margin);
279            self.fixed += border.top + style.padding.top;
280        }
281        if style.sized() {
282            let within = self.tall_within();
283            let definite = style.height.resolve(within);
284            let least = style
285                .min_height
286                .resolve(within)
287                .unwrap_or(0.0)
288                .max(definite.unwrap_or(0.0));
289            self.tall.push(Tall {
290                top: self.depth + self.fixed + self.margin,
291                definite,
292                least,
293            });
294        }
295        start
296    }
297
298    /// The height a percentage height measures against: the nearest
299    /// definite `height` around the block, or the page's content box.
300    fn tall_within(&self) -> f32 {
301        self.tall
302            .iter()
303            .rev()
304            .find_map(|tall| tall.definite)
305            .unwrap_or_else(|| self.styles().default_page().geometry.content_size().1)
306    }
307
308    /// Commits space no margin collapses through. The margin a
309    /// border or padding shuts off is outside every block that has
310    /// not opened a box of its own yet, and inside every block that
311    /// has.
312    fn commit(&mut self, amount: f32) {
313        for pending in self.open.iter_mut().rev() {
314            if pending.started {
315                break;
316            }
317            pending.started = true;
318            pending.open_fixed += amount;
319        }
320        self.fixed += amount;
321    }
322
323    /// Resolves what one element sets — its `string-set` values, the
324    /// folio its page takes — against its own text. It lands on the
325    /// first fragment the element emits, which is the fragment whose
326    /// page the element is on.
327    ///
328    /// An element that emits nothing hands what it set to whatever
329    /// comes next: a string set by an empty heading is still set.
330    fn mark(&mut self, style: &ComputedStyle, inlines: &[Inline]) {
331        if style.string_set.is_empty() && style.counter_reset.is_none() {
332            return;
333        }
334        let mut cached = None;
335        let marks = self.pending_marks.get_or_insert_with(Box::default);
336        for set in &style.string_set {
337            let mut value = String::new();
338            for piece in &set.value {
339                match piece {
340                    StringPiece::Content => {
341                        value.push_str(cached.get_or_insert_with(|| text(inlines)))
342                    }
343                    StringPiece::Text(literal) => value.push_str(literal),
344                }
345            }
346            marks.strings.push((set.name.clone(), value));
347        }
348        if let Some(folio) = style.counter_reset {
349            marks.page_number = Some(folio);
350        }
351    }
352
353    /// Records a node a link can reach. It lands on the first fragment
354    /// the node emits, the way a string it sets does, so the page that
355    /// fragment lands on is the page a reference to the node prints.
356    pub(super) fn name(&mut self, node: NodeId) {
357        let marks = self.pending_marks.get_or_insert_with(Box::default);
358        marks.targets.push(node);
359    }
360
361    /// The same for the inlines of one block: an inline that carries
362    /// an id is a target, and one that sets a string resolves it
363    /// against its own text. Both land on the block's first line.
364    fn name_inlines(&mut self, inlines: &[Inline]) {
365        for inline in inlines {
366            let node = inline_id(inline);
367            if inline_attributes(inline).id.is_some() {
368                self.name(node);
369            }
370            let style = self.styles().style(node);
371            if !style.string_set.is_empty() || style.counter_reset.is_some() {
372                let style = style.clone();
373                self.mark(&style, std::slice::from_ref(inline));
374            }
375            if let Inline::Emphasis { children, .. }
376            | Inline::Strong { children, .. }
377            | Inline::Link { children, .. } = inline
378            {
379                self.name_inlines(children);
380            }
381        }
382    }
383
384    /// Closes a block: `break-inside: avoid` glues everything it
385    /// emitted, its bottom border and padding take height of their
386    /// own, and its bottom margin becomes the next block's lead.
387    pub(super) fn close(&mut self, style: &ComputedStyle, start: usize) {
388        if style.sized() {
389            let tall = self.tall.pop().expect("the block opened a height");
390            let rest = tall.least - (self.depth + self.fixed + self.margin - tall.top);
391            if rest > 0.0 {
392                self.remainder(rest, start);
393            }
394        }
395        if style.break_inside == Break::Avoid {
396            for fragment in self.fragments.iter_mut().skip(start + 1) {
397                fragment.break_before = BreakPoint::Forbidden;
398            }
399        }
400        let border = style.border.widths();
401        if border.bottom + style.padding.bottom > 0.0 {
402            let margin = std::mem::take(&mut self.margin);
403            self.commit(margin);
404            self.fixed += border.bottom + style.padding.bottom;
405        }
406        let pending = self.open.pop().expect("the block opened a box");
407        self.seal(pending);
408        if style.position == Position::Relative
409            && let Some(offset) = self.moved.pop()
410        {
411            self.offset = offset;
412        }
413        if let Some(opacity) = self.faded.pop() {
414            self.opacity = opacity;
415        }
416        self.margin = self.margin.max(style.margin.bottom);
417        // A block that emitted nothing settles nothing: what was
418        // asked above it is still asked above whatever comes next. A
419        // forced break its last block asked for is asked after it too.
420        if self.fragments.len() > start && !matches!(self.pending, BreakPoint::Forced(_)) {
421            self.pending = BreakPoint::Allowed;
422        }
423        self.ask(style.break_after);
424    }
425
426    /// The space a block taller than its content leaves below that
427    /// content, as a fragment of the block. A page does not end
428    /// between the content and the space. A block that emitted nothing
429    /// else takes what was asked above it here.
430    fn remainder(&mut self, rest: f32, start: usize) {
431        let mut blank = Fragment::plain(0.0, rest, Piece::Blank);
432        let mut first = self.fragments[start..]
433            .iter()
434            .all(|fragment| matches!(fragment.piece, Piece::Anchor(_)));
435        if !first {
436            blank.break_before = BreakPoint::Forbidden;
437            blank.lead = std::mem::take(&mut self.margin);
438            blank.fixed = std::mem::take(&mut self.fixed);
439        }
440        self.emit(&mut first, blank);
441    }
442
443    /// Hands one block's decoration to the fragments at the ends of
444    /// its range, which is where the paginator reads it back. A block
445    /// that emitted nothing has no range and paints nothing. The
446    /// boxes it wrote against the page are not fragments of it, so
447    /// its range ends at the last fragment the flow places.
448    fn seal(&mut self, pending: Pending) {
449        let placed = |fragment: &Fragment| !matches!(fragment.piece, Piece::Anchor(_));
450        let Some(last) = self.fragments[pending.start..].iter().rposition(placed) else {
451            return;
452        };
453        let end = pending.start + last + 1;
454        let mut decoration = pending.decoration;
455        // Everything committed since the last fragment was emitted
456        // lies inside the block that fragment was in.
457        decoration.below = self.fixed;
458        // Outermost first, so the paginator's stack pops the
459        // innermost block that ends at a fragment.
460        Builder::decorations(&mut self.fragments[pending.start])
461            .opens
462            .insert(0, decoration);
463        Builder::decorations(&mut self.fragments[end - 1]).closes += 1;
464    }
465
466    fn decorations(fragment: &mut Fragment) -> &mut Decorations {
467        fragment.decorations.get_or_insert_with(Box::default)
468    }
469
470    /// Emits the one fragment a block is: everything the cascade asked
471    /// for above the block goes on it, there being no other fragment
472    /// for it.
473    fn emit_one(&mut self, x: f32, height: f32, piece: Piece) {
474        self.emit(&mut true, Fragment::plain(x, height, piece));
475    }
476
477    /// Emits one fragment. The first of a block gets the break the
478    /// cascade asked for above it and the space its margins left; the
479    /// rest keep what they arrived with, which is what the block says
480    /// about splitting itself.
481    pub(super) fn emit(&mut self, first: &mut bool, mut fragment: Fragment) {
482        // This fragment settles how far the border box of every
483        // block opening on it sits above it.
484        let (index, fixed) = (self.fragments.len(), self.fixed);
485        for pending in &mut self.open {
486            if pending.start == index {
487                pending.decoration.above = fixed - pending.open_fixed;
488            }
489        }
490        if *first {
491            *first = false;
492            fragment.break_before = std::mem::replace(&mut self.pending, BreakPoint::Allowed);
493            fragment.lead = std::mem::take(&mut self.margin);
494            fragment.fixed = std::mem::take(&mut self.fixed);
495            fragment.marks = self.pending_marks.take();
496        }
497        fragment.spanning = self.spanning;
498        fragment.layer = self.layer;
499        fragment.offset = self.offset;
500        fragment.opacity = self.opacity;
501        self.depth += fragment.lead + fragment.fixed + fragment.height;
502        self.fragments.push(fragment);
503    }
504
505    /// Marks where an image the sheet lifted out of the flow was
506    /// written. The fragment takes no space. The page the flow
507    /// reaches when it passes here is the page that carries the
508    /// image.
509    ///
510    /// A box against the page is no fragment of the block it was
511    /// written in. A block whose first fragment this would be starts
512    /// at the next fragment instead, so that its border box opens on
513    /// one the flow places.
514    pub(super) fn anchor(&mut self, id: NodeId) {
515        let index = self.fragments.len();
516        for pending in &mut self.open {
517            if pending.start == index {
518                pending.start = index + 1;
519            }
520        }
521        self.fragments
522            .push(Fragment::plain(0.0, 0.0, Piece::Anchor(id)));
523    }
524
525    /// Everything built so far, stacked in a box that no page break
526    /// splits. The result holds what the blocks paint, from the top
527    /// and the leading edge of that box, and the height of the box.
528    ///
529    /// A table cell is one of these, and so is a block anchored to
530    /// the page.
531    pub(super) fn stack(mut self) -> Stacked {
532        let mut marks = None;
533        let mut anchors = Vec::new();
534        let mut notes = Vec::new();
535        let mut placed = Vec::new();
536        let mut cursor = 0.0f32;
537        for fragment in &self.fragments {
538            if let Piece::Anchor(node) = fragment.piece {
539                anchors.push(node);
540                continue;
541            }
542            if let Some(held) = &fragment.notes {
543                notes.extend(held.iter().cloned());
544            }
545            gather(&mut marks, fragment.marks.clone());
546            let top = cursor + fragment.lead + fragment.fixed;
547            placed.push((top, fragment));
548            cursor = top + fragment.height;
549        }
550        gather(&mut marks, self.pending_marks.take());
551        let (mut items, boxes) = decorate(&placed);
552        for (top, fragment) in &placed {
553            items.append(&mut self.paginator.fragment_items(fragment, 0.0, *top));
554        }
555        Stacked {
556            items,
557            boxes,
558            height: cursor + self.margin + self.fixed,
559            anchors,
560            notes,
561            marks,
562        }
563    }
564
565    /// Every block of one nesting level, at `x` from the content
566    /// box's leading edge and breaking to `measure`.
567    pub(super) fn blocks<'b>(
568        &mut self,
569        boxes: impl IntoIterator<Item = Child<'b>>,
570        x: f32,
571        measure: f32,
572    ) {
573        for child in boxes {
574            let id = child.id();
575            if let Child::Block(block) = child
576                && (matches!(block, Block::Heading { .. }) || block_attributes(block).id.is_some())
577            {
578                self.name(id);
579            }
580            // A box against the page is not in the flow: it takes no
581            // space here, and the margins that met around it still
582            // meet.
583            if self.lifted != Some(id) && self.styles().style(id).position == Position::Absolute {
584                self.anchor(id);
585                continue;
586            }
587            let block = match child {
588                Child::Block(block) => block,
589                Child::Generated(id, position) => {
590                    self.generated(id, position, x, measure);
591                    continue;
592                }
593            };
594            let styles = self.paginator.styles;
595            let position = block_position(block);
596            match block {
597                Block::Heading { id, inlines, .. } | Block::Paragraph { id, inlines, .. } => {
598                    self.name_inlines(inlines);
599                    self.paragraph(*id, inlines, position, x, measure);
600                }
601                Block::Blockquote { id, blocks, .. } => {
602                    let style = styles.style(*id).clone();
603                    let start = self.open(*id, &style, &[], x, measure);
604                    let (inner, narrowed) = style.content_box(x, measure);
605                    self.blocks(children(styles, *id, blocks, position), inner, narrowed);
606                    self.close(&style, start);
607                }
608                Block::CodeBlock { id, text, .. } => {
609                    self.code_block(*id, text, position, x, measure)
610                }
611                Block::ThematicBreak { id, .. } => {
612                    let style = styles.style(*id).clone();
613                    let start = self.open(*id, &style, &[], x, measure);
614                    let (inner, narrowed) = style.content_box(x, measure);
615                    self.pseudo(*id, PseudoElement::Before, position, inner, narrowed);
616                    self.ornament(&style, x, measure);
617                    self.pseudo(*id, PseudoElement::After, position, inner, narrowed);
618                    self.close(&style, start);
619                }
620                // A break emits nothing of its own, so what it asks
621                // for lands on whatever comes next.
622                Block::PageBreak { id, .. } | Block::ColumnBreak { id, .. } => {
623                    let style = styles.style(*id).clone();
624                    let start = self.open(*id, &style, &[], x, measure);
625                    let (inner, narrowed) = style.content_box(x, measure);
626                    self.pseudo(*id, PseudoElement::Before, position, inner, narrowed);
627                    self.pseudo(*id, PseudoElement::After, position, inner, narrowed);
628                    self.close(&style, start);
629                }
630                Block::Image { id, url, .. } => {
631                    let style = styles.style(*id).clone();
632                    // The image takes its `height` itself. A scaled
633                    // image would otherwise leave the rest below it.
634                    let boxed = ComputedStyle {
635                        height: crate::style::Width::Auto,
636                        ..style.clone()
637                    };
638                    let start = self.open(*id, &boxed, &[], x, measure);
639                    let (inner, narrowed) = style.content_box(x, measure);
640                    self.pseudo(*id, PseudoElement::Before, position, inner, narrowed);
641                    self.image(&style, url, origin(self.source, position), x, measure);
642                    self.pseudo(*id, PseudoElement::After, position, inner, narrowed);
643                    self.close(&boxed, start);
644                }
645                Block::List {
646                    id,
647                    start,
648                    items,
649                    position,
650                    ..
651                } => self.list(*id, *start, items, *position, x, measure),
652                Block::Table {
653                    id,
654                    head,
655                    body,
656                    position,
657                    ..
658                } => self.table(*id, head, body, *position, x, measure),
659            }
660        }
661    }
662
663    /// One paragraph's lines as fragments: alignment against each
664    /// band, the initial letter beside the first of them, and where a
665    /// page can end between them.
666    ///
667    /// `spec` is the profile the lines were broken to, and `gaps` the
668    /// space above a band the profile had to move past an image.
669    fn paragraph(
670        &mut self,
671        id: NodeId,
672        inlines: &[Inline],
673        position: Option<SourcePos>,
674        x: f32,
675        measure: f32,
676    ) {
677        let computed = self.styles().style(id).clone();
678        let start = self.open(id, &computed, inlines, x, measure);
679        let (x, measure) = computed.content_box(x, measure);
680        self.pseudo(id, PseudoElement::Before, position, x, measure);
681
682        let style = computed.paragraph();
683        let options = self.options(&computed);
684        let cap = self.paginator.drop_cap(id, &computed, inlines);
685        let full = Span::band(0.0, measure);
686        let spec = match &cap {
687            Some((cap, _)) => Measure::new(
688                vec![Span::ending(measure, measure - cap.reserved); cap.lines],
689                full,
690            ),
691            // An indent is a shorter first line. A cap outranks it:
692            // the first line is already displaced, and a book does
693            // not indent the paragraph a chapter opens with.
694            None if computed.text_indent != 0.0 => Measure::new(
695                vec![Span::ending(measure, measure - computed.text_indent)],
696                full,
697            ),
698            None => Measure::uniform(measure),
699        };
700        // The letter the cap holds is passed over below, so a drop
701        // cap and a first line over the same paragraph divide it:
702        // `::first-letter` has the cap, `::first-line` the rest of
703        // the line beside it.
704        let opening = Opening {
705            first_line: self.styles().opening_line(id),
706            taken: cap.as_ref().map_or(0, |(_, taken)| *taken),
707            node: id,
708        };
709        // The notes are built before the lines, because the number
710        // of each is what its reference prints in one of them.
711        let notes: Vec<Arc<Note>> = notes_in_inlines(inlines)
712            .into_iter()
713            .filter_map(|note| self.paginator.note(note, self.source))
714            .collect();
715        let referring = Referring {
716            paginator: self.paginator,
717            source: self.source,
718        };
719        let (broken, shaped) = self
720            .paginator
721            .lines
722            .layout_shaped(inlines, &style, &referring, &spec, options, opening);
723
724        let setting = Setting {
725            x,
726            align: computed.text_align,
727            orphans: computed.orphans as usize,
728            widows: computed.widows as usize,
729            cap: cap.map(|(cap, _)| cap),
730            cap_x: 0.0,
731            notes,
732        };
733        let fragments = set_lines(self.paginator, broken.lines, &spec, &[], &setting);
734        let reflow = shaped.filter(|_| self.paginator.wraps()).map(|shaped| {
735            Arc::new(Reflow {
736                leading: self.paginator.lines.strut(&style).height(),
737                shaped,
738                setting,
739                base: spec,
740                ends: broken.ends,
741            })
742        });
743        let mut first = true;
744        for mut fragment in fragments {
745            fragment.reflow = reflow.clone();
746            self.emit(&mut first, fragment);
747        }
748        self.pseudo(id, PseudoElement::After, position, x, measure);
749        self.close(&computed, start);
750    }
751
752    /// How the lines of a block in `style` break.
753    fn options(&self, style: &ComputedStyle) -> LineBreakOptions {
754        let hyphenate = style.hyphens == Hyphens::Auto;
755        LineBreakOptions {
756            hyphenate,
757            patterns: if hyphenate {
758                self.paginator.patterns()
759            } else {
760                Patterns::NONE
761            },
762            justify: style.text_align == TextAlign::Justify,
763            inter_character: style.text_justify == TextJustify::InterCharacter,
764            hanging: style.hanging_punctuation,
765            preformatted: false,
766        }
767    }
768
769    /// One code block's lines as fragments.
770    ///
771    /// The text breaks at its own newlines and nowhere else. Nothing
772    /// in it hyphenates, no line of it is justified, no mark of it
773    /// hangs past the measure, and its first line carries no indent,
774    /// whatever the cascade asked for around it. A character the
775    /// author did not put where the reader finds it changes what the
776    /// code says.
777    fn code_block(
778        &mut self,
779        id: NodeId,
780        text: &str,
781        position: Option<SourcePos>,
782        x: f32,
783        measure: f32,
784    ) {
785        let computed = self.styles().style(id).clone();
786        let start = self.open(id, &computed, &[], x, measure);
787        let (x, measure) = computed.content_box(x, measure);
788        self.pseudo(id, PseudoElement::Before, position, x, measure);
789
790        let style = computed.paragraph();
791        let options = LineBreakOptions {
792            preformatted: true,
793            ..LineBreakOptions::default()
794        };
795        let spec = Measure::uniform(measure);
796        let lines = self
797            .paginator
798            .lines
799            .layout_preformatted(text, id, &style, &spec, options);
800        let over = lines
801            .iter()
802            .any(|line| self.paginator.line_width(line) > measure);
803        if over {
804            let at = origin(self.source, position);
805            let message = concat!(
806                "A line of a code block is wider than the measure. It runs past it, ",
807                "because a code block breaks only where its own text does.",
808            );
809            self.paginator
810                .warn(message.to_string(), (!at.is_empty()).then_some(at));
811        }
812        let setting = Setting {
813            x,
814            align: computed.text_align,
815            orphans: computed.orphans as usize,
816            widows: computed.widows as usize,
817            cap: None,
818            cap_x: 0.0,
819            notes: Vec::new(),
820        };
821        if lines.is_empty() {
822            self.emit_one(x, 0.0, Piece::Blank);
823        }
824        let mut first = true;
825        for fragment in set_lines(self.paginator, lines, &spec, &[], &setting) {
826            self.emit(&mut first, fragment);
827        }
828        self.pseudo(id, PseudoElement::After, position, x, measure);
829        self.close(&computed, start);
830    }
831
832    /// The children of the block `element` that `::before` or
833    /// `::after` generates, at `x` and `measure`. `position` is where
834    /// the block was written.
835    pub(super) fn pseudo(
836        &mut self,
837        element: NodeId,
838        which: PseudoElement,
839        position: Option<SourcePos>,
840        x: f32,
841        measure: f32,
842    ) {
843        let child = self
844            .styles()
845            .pseudo_element(element, which)
846            .map(|id| Child::Generated(id, position));
847        self.blocks(child, x, measure);
848    }
849
850    /// The generated box `id` in the flow, across the content box at
851    /// `x` and `measure`. `position` is where its element was written.
852    /// It holds the text of its `content`, or nothing. A page does not
853    /// end between the box and the rest of its element.
854    fn generated(&mut self, id: NodeId, position: Option<SourcePos>, x: f32, measure: f32) {
855        let Some((_, which)) = id.pseudo_element() else {
856            return;
857        };
858        let mut style = self.styles().style(id).clone();
859        // A box with no layer of its own paints in the layer of its
860        // block.
861        let layer = self.layer;
862        if style.z_index == 0 {
863            style.z_index = layer;
864        }
865        if which == PseudoElement::After {
866            self.ask(Break::Avoid);
867        }
868        let start = self.open(id, &style, &[], x, measure);
869        let (x, measure) = style.content_box(x, measure);
870        let text = self
871            .paginator
872            .generate(&style.content, None, self.source, position)
873            .unwrap_or_default();
874        let spec = Measure::uniform(measure);
875        let lines = if text.is_empty() {
876            Vec::new()
877        } else {
878            let options = self.options(&style);
879            self.paginator
880                .lines
881                .layout_generated(&text, id, &style.paragraph(), &spec, options)
882        };
883        if lines.is_empty() {
884            self.emit_one(x, 0.0, Piece::Blank);
885        } else {
886            let setting = Setting {
887                x,
888                align: style.text_align,
889                orphans: style.orphans as usize,
890                widows: style.widows as usize,
891                cap: None,
892                cap_x: 0.0,
893                notes: Vec::new(),
894            };
895            let mut first = true;
896            for fragment in set_lines(self.paginator, lines, &spec, &[], &setting) {
897                self.emit(&mut first, fragment);
898            }
899        }
900        self.close(&style, start);
901        self.layer = layer;
902        if which == PseudoElement::Before {
903            self.ask(Break::Avoid);
904        }
905    }
906
907    /// A thematic break: the ornament the cascade named, or the space
908    /// it leaves when it names none.
909    fn ornament(&mut self, style: &ComputedStyle, x: f32, measure: f32) {
910        let (x, measure) = style.content_box(x, measure);
911        let paragraph = style.paragraph();
912        let ornament = match &style.content {
913            Content::Text(text) if !text.is_empty() => {
914                self.paginator.line_of(text, &paragraph).map(|line| {
915                    let offset =
916                        align_offset(style.text_align, self.paginator.line_width(&line), measure);
917                    (offset, line.box_.height, Piece::Line { line, cap: None })
918                })
919            }
920            _ => None,
921        };
922        let (offset, height, piece) = ornament.unwrap_or_else(|| {
923            (
924                0.0,
925                self.paginator.lines.strut(&paragraph).height(),
926                Piece::Blank,
927            )
928        });
929        self.emit_one(x + offset, height, piece);
930    }
931
932    /// A block image, at the size its style and its file ask for,
933    /// scaled down when that does not fit the page. A percentage width
934    /// measures against `measure`, the width the image is laid out in.
935    fn image(&mut self, style: &ComputedStyle, url: &str, origin: String, x: f32, measure: f32) {
936        let Some((asset, intrinsic)) = self.paginator.assets.lookup(url) else {
937            self.paginator.missing(url, origin);
938            return;
939        };
940        let within = (measure, self.tall_within());
941        let (x, measure) = style.content_box(x, measure);
942        let available = self
943            .paginator
944            .styles
945            .default_page()
946            .geometry
947            .content_size()
948            .1;
949        let ImageSize { width, height, .. } = self.paginator.image_size(
950            style,
951            url,
952            intrinsic.size(),
953            within,
954            measure,
955            available,
956            (!origin.is_empty()).then_some(origin),
957        );
958        let offset = align_offset(style.text_align, width, measure);
959        self.emit_one(
960            x + offset,
961            height,
962            Piece::Image {
963                width,
964                height,
965                asset,
966            },
967        );
968    }
969}
970
971/// One paragraph's lines as fragments: alignment against each band,
972/// the initial letter beside the first of them, and where a page may
973/// end between them.
974///
975/// `spec` is the profile the lines were broken to, and `gaps` the
976/// space above a band the profile had to move past an image.
977pub(super) fn set_lines(
978    paginator: &Paginator,
979    lines: Vec<Line>,
980    spec: &Measure,
981    gaps: &[f32],
982    setting: &Setting,
983) -> Vec<Fragment> {
984    let count = lines.len();
985    let sunk = setting
986        .cap
987        .as_ref()
988        .map(|cap| cap.lines.min(count))
989        .unwrap_or(0);
990    // The cap's baseline is the last sunk line's, which is only
991    // known once the lines are broken.
992    let drop = if sunk > 0 {
993        lines[1..sunk]
994            .iter()
995            .map(|line| line.box_.height)
996            .sum::<f32>()
997            + lines[sunk - 1].box_.baseline
998            - lines[0].box_.baseline
999    } else {
1000        0.0
1001    };
1002    let mut cap = setting.cap.as_ref().map(|cap| {
1003        Box::new(DropCap {
1004            line: cap.line.clone(),
1005            x: setting.x + setting.cap_x,
1006            drop,
1007        })
1008    });
1009
1010    let mut fragments = Vec::with_capacity(count);
1011    let mut slot = 0;
1012    for (index, mut line) in lines.into_iter().enumerate() {
1013        let last = line.spans.len() - 1;
1014        // A band's slack is at the end of its last span, so that
1015        // is where alignment moves the text.
1016        let offset = align_offset(
1017            setting.align,
1018            paginator.span_width(&line, last),
1019            spec.at(slot + last).width,
1020        );
1021        line.spans[last].offset += offset;
1022        let origin = spec.at(slot).origin;
1023        slot += line.spans.len();
1024        let height = line.box_.height;
1025        let protrusion = line.protrusion;
1026        let notes: Vec<Arc<Note>> = setting
1027            .notes
1028            .iter()
1029            .filter(|note| line.runs.iter().any(|run| run.inline == Some(note.node)))
1030            .cloned()
1031            .collect();
1032        let piece = Piece::Line {
1033            line,
1034            cap: (index == 0).then(|| cap.take()).flatten(),
1035        };
1036        let mut fragment = Fragment::plain(setting.x + origin - protrusion, height, piece);
1037        if !notes.is_empty() {
1038            fragment.notes = Some(Box::new(notes));
1039        }
1040        fragment.break_before =
1041            if index < setting.orphans || count - index < setting.widows || index < sunk {
1042                BreakPoint::Forbidden
1043            } else {
1044                BreakPoint::Allowed
1045            };
1046        fragment.fixed = gaps.get(index).copied().unwrap_or(0.0);
1047        fragments.push(fragment);
1048    }
1049    fragments
1050}
1051
1052/// Moves what the flow reads off the fragments a paragraph arrived as
1053/// onto the ones it was set again as: the space and the break above
1054/// the first of them, what it tells the page furniture, and the
1055/// decorations that open and close over the paragraph.
1056///
1057/// `fixed` is the paragraph's own space above the first of them.
1058pub(super) fn carry_over(fresh: &mut [Fragment], old: &[Fragment], fixed: f32) {
1059    let (Some(head), Some(last)) = (old.first(), old.last()) else {
1060        return;
1061    };
1062    for fragment in fresh.iter_mut() {
1063        fragment.spanning = head.spanning;
1064        fragment.layer = head.layer;
1065        fragment.offset = head.offset;
1066        fragment.opacity = head.opacity;
1067    }
1068    let opens = head
1069        .decorations
1070        .as_ref()
1071        .map(|decorations| decorations.opens.clone())
1072        .unwrap_or_default();
1073    let closes = last
1074        .decorations
1075        .as_ref()
1076        .map(|decorations| decorations.closes)
1077        .unwrap_or(0);
1078    if let Some(first) = fresh.first_mut() {
1079        first.break_before = head.break_before;
1080        first.lead = head.lead;
1081        first.fixed += fixed;
1082        first.marks = head.marks.clone();
1083        first.markers = head.markers.clone();
1084        if !opens.is_empty() {
1085            first.decorations = Some(Box::new(Decorations {
1086                opens,
1087                ..Default::default()
1088            }));
1089        }
1090    }
1091    if let Some(end) = fresh.last_mut()
1092        && closes > 0
1093    {
1094        end.decorations.get_or_insert_with(Box::default).closes += closes;
1095    }
1096}
1097
1098/// What one paragraph's lines are set against once they are broken:
1099/// where they start, how they fill a band, and what must not be split
1100/// from what.
1101#[derive(Debug, Clone)]
1102pub(super) struct Setting {
1103    /// Leading edge, from the content box's own.
1104    pub(super) x: f32,
1105    align: TextAlign,
1106    orphans: usize,
1107    widows: usize,
1108    /// The initial letter beside the lines it is sunk over.
1109    pub(super) cap: Option<Cap>,
1110    /// Where the letter goes, from `x`. An image in the way of the
1111    /// bands it is sunk over moves it along with them.
1112    cap_x: f32,
1113    /// The notes written in the paragraph, in reading order. Each one
1114    /// lands on the line its reference was set on, and travels with
1115    /// that line.
1116    notes: Vec<Arc<Note>>,
1117}
1118
1119impl Setting {
1120    /// The same over the bands a profile left. An initial letter
1121    /// belongs to the line a paragraph opens on rather than to the
1122    /// line the rest of it opens on, and an image can move it.
1123    pub(super) fn wrapped(&self, opening: bool, letter: Option<f32>) -> Cow<'_, Setting> {
1124        if opening && letter.is_none() {
1125            return Cow::Borrowed(self);
1126        }
1127        Cow::Owned(Setting {
1128            cap: opening.then(|| self.cap.clone()).flatten(),
1129            cap_x: letter.unwrap_or(0.0),
1130            ..self.clone()
1131        })
1132    }
1133}
1134
1135/// A paragraph the flow can set again.
1136///
1137/// The lines a section is built with are broken against the measure
1138/// with nothing in the way. A paragraph that lands beside an image is
1139/// broken again against the bands the image leaves. This is what that
1140/// takes: the shaped runs, which the measure has no say in, and
1141/// everything settled around them.
1142#[derive(Debug)]
1143pub struct Reflow {
1144    pub(super) shaped: Shaped,
1145    pub(super) setting: Setting,
1146    /// The bands with nothing in the way. A first-line indent and a
1147    /// drop cap are already in them.
1148    pub(super) base: Measure,
1149    /// Height of one band, which is what an image is snapped to.
1150    pub(super) leading: f32,
1151    /// Where each line ended, so the rest of the paragraph can be set
1152    /// from any of them.
1153    pub(super) ends: Vec<usize>,
1154}
1155
1156/// What a stack of blocks comes to.
1157pub(super) struct Stacked {
1158    /// What they paint, from the top of their box.
1159    pub(super) items: Vec<DrawItem>,
1160    /// The border boxes of the blocks, from the top of their box.
1161    pub(super) boxes: Vec<(NodeId, PageBox)>,
1162    /// Their height, margins included.
1163    pub(super) height: f32,
1164    /// The boxes the sheet lifted out of the flow from inside them.
1165    pub(super) anchors: Vec<NodeId>,
1166    /// The notes written inside them, which are set at the foot of
1167    /// the page the stack lands on.
1168    pub(super) notes: Vec<Arc<Note>>,
1169    /// The strings, folios, and targets the blocks give the page
1170    /// furniture.
1171    pub(super) marks: Option<Box<Marks>>,
1172}
1173
1174/// The decorated blocks inside one stack, as the rects they paint. A
1175/// stack is never split, so no box inside it is cut.
1176pub(super) fn decorate(placed: &[(f32, &Fragment)]) -> (Vec<DrawItem>, Vec<(NodeId, PageBox)>) {
1177    let mut boxes: Vec<Painted> = Vec::new();
1178    let mut open: Vec<usize> = Vec::new();
1179    for (top, fragment) in placed {
1180        let Some(decorations) = &fragment.decorations else {
1181            continue;
1182        };
1183        for decoration in &decorations.opens {
1184            open.push(boxes.len());
1185            boxes.push(Painted {
1186                top: top - decoration.above,
1187                decoration: decoration.clone(),
1188                bottom: 0.0,
1189                cut_above: false,
1190                cut_below: false,
1191            });
1192        }
1193        for _ in 0..decorations.closes {
1194            let Some(index) = open.pop() else { continue };
1195            boxes[index].bottom = top + fragment.height + boxes[index].decoration.below;
1196        }
1197    }
1198    // A block the stack does not finish ends at the last fragment of
1199    // it that is here. A note split across two pages is one.
1200    let last = placed
1201        .last()
1202        .map(|(top, fragment)| top + fragment.height)
1203        .unwrap_or(0.0);
1204    for index in open {
1205        boxes[index].bottom = last;
1206        boxes[index].cut_below = true;
1207    }
1208    let mut items = Vec::new();
1209    let mut areas = Vec::new();
1210    for painted in &boxes {
1211        let (x, y, width, height) = painted.border_box((0.0, 0.0));
1212        if height > 0.0 {
1213            let area = PageBox {
1214                page: 0,
1215                x,
1216                y,
1217                width,
1218                height,
1219            };
1220            areas.push((painted.decoration.node, area));
1221        }
1222        items.extend(painted.items((0.0, 0.0)));
1223    }
1224    (items, areas)
1225}
1226
1227/// Adds the marks of one fragment to the marks gathered so far.
1228pub(super) fn gather(into: &mut Option<Box<Marks>>, from: Option<Box<Marks>>) {
1229    let Some(from) = from else {
1230        return;
1231    };
1232    let marks = into.get_or_insert_with(Box::default);
1233    marks.strings.extend(from.strings);
1234    marks.page_number = from.page_number.or(marks.page_number);
1235    marks.targets.extend(from.targets);
1236}
1237
1238/// Where a line of `width` starts inside a measure of `available`.
1239fn align_offset(align: TextAlign, width: f32, available: f32) -> f32 {
1240    match align {
1241        TextAlign::Left | TextAlign::Justify => 0.0,
1242        TextAlign::Right => (available - width).max(0.0),
1243        TextAlign::Center => ((available - width) / 2.0).max(0.0),
1244    }
1245}
1246
1247#[cfg(test)]
1248mod tests {
1249    use crate::content::{Attributes, Block, HeadingLevel, NodeId, Section, SourcePos};
1250    use crate::layout::testing::{
1251        assert_orphans_and_widows, book_of, broken_heading, chapter_size, content_lines,
1252        folio_size, heading, long_prose, master, origin_of, ornament, paginate, paginate_styled,
1253        paragraph, png, prose, quote, rects, registry, right_edge, scene_break, section,
1254        small_caps_lines, styled, tagged_prose, ua, under_h3,
1255    };
1256    use crate::layout::{BreakPoint, Fragment, Paginator, Piece, layout_book};
1257    use crate::pages::{DrawItem, Page, Side};
1258    use crate::style::{Color, Situation};
1259
1260    /// The baseline of the first line of the first page that opens
1261    /// with `token`.
1262    fn baseline_of(pages: &[Page], token: &str) -> f32 {
1263        content_lines(&pages[0])
1264            .into_iter()
1265            .find(|(_, runs)| runs[0].2.starts_with(token))
1266            .map(|(baseline, _)| baseline)
1267            .unwrap_or_else(|| panic!("no line opens with {token}"))
1268    }
1269
1270    /// Acceptance: `h1 { height: 3in }` leaves three inches before the
1271    /// block under it, whatever the heading's own height.
1272    #[test]
1273    fn a_heading_with_a_height_leaves_that_height_before_the_block_under_it() {
1274        let alone = paginate(vec![section(vec![paragraph("under the heading")])]);
1275        let wanted = baseline_of(&alone, "under") + 216.0;
1276        for css in [
1277            "h1 { height: 3in }",
1278            "h1 { height: 3in; font-size: 40pt }",
1279            "h1 { height: 3in; font-size: 9pt }",
1280        ] {
1281            let pages = paginate_styled(
1282                css,
1283                vec![section(vec![
1284                    heading("Tall"),
1285                    paragraph("under the heading"),
1286                ])],
1287            );
1288            let under = baseline_of(&pages, "under");
1289            assert!(
1290                (under - wanted).abs() < 1e-3,
1291                "{css}: the paragraph sits on {under}, not {wanted}"
1292            );
1293        }
1294    }
1295
1296    /// Acceptance: a percentage height resolves against the content box
1297    /// the block is in. For a block of the section that is the page's,
1298    /// and for a paragraph inside a quotation with a height it is the
1299    /// quotation's.
1300    #[test]
1301    fn a_percentage_height_resolves_against_the_content_box_the_block_is_in() {
1302        let (_, area) = ua().default_page().geometry.content_size();
1303        let alone = paginate(vec![section(vec![paragraph("after")])]);
1304        let top = baseline_of(&alone, "after");
1305        let near = |a: f32, b: f32| (a - b).abs() < 1e-3;
1306
1307        let half = paginate_styled(
1308            "h1 { height: 50% }",
1309            vec![section(vec![heading("Half"), paragraph("after")])],
1310        );
1311        assert!(near(baseline_of(&half, "after"), top + area / 2.0));
1312
1313        let nested = paginate_styled(
1314            "blockquote { height: 4in; margin: 0 } blockquote p { height: 50% }",
1315            vec![section(vec![
1316                quote(vec![paragraph("first"), paragraph("second")]),
1317                paragraph("after"),
1318            ])],
1319        );
1320        assert!(near(baseline_of(&nested, "first"), top));
1321        assert!(near(baseline_of(&nested, "second"), top + 144.0));
1322        assert!(near(baseline_of(&nested, "after"), top + 288.0));
1323    }
1324
1325    /// Part: a block taller than its content leaves the remainder below
1326    /// the content, inside its own box. `min-height` does the same for
1327    /// a block whose content is shorter. A block whose content is
1328    /// taller than its height grows to hold it.
1329    #[test]
1330    fn a_block_taller_than_its_content_leaves_the_remainder_below_it() {
1331        use crate::layout::testing::rects;
1332        let sections = || {
1333            vec![section(vec![
1334                quote(vec![paragraph("quoted")]),
1335                paragraph("after"),
1336            ])]
1337        };
1338        let near = |a: f32, b: f32| (a - b).abs() < 1e-3;
1339        let plain = paginate_styled("blockquote { margin: 0 }", sections());
1340        let quoted = baseline_of(&plain, "quoted");
1341
1342        let tall = paginate_styled(
1343            "blockquote { margin: 0; height: 2in; background-color: #eeeeee }",
1344            sections(),
1345        );
1346        assert!(near(baseline_of(&tall, "quoted"), quoted));
1347        assert!(near(baseline_of(&tall, "after"), quoted + 144.0));
1348        let tint = rects(&tall[0]);
1349        assert_eq!(tint.len(), 1, "one box, one tint: {tint:?}");
1350        assert!(near(tint[0].3, 144.0), "the box is {} tall", tint[0].3);
1351
1352        let least = paginate_styled("blockquote { margin: 0; min-height: 2in }", sections());
1353        assert!(near(baseline_of(&least, "after"), quoted + 144.0));
1354
1355        let short = paginate_styled("blockquote { margin: 0; height: 1pt }", sections());
1356        assert_eq!(
1357            baseline_of(&short, "after"),
1358            baseline_of(&plain, "after"),
1359            "a block shorter than its content cut into it"
1360        );
1361    }
1362
1363    /// A block that spans the columns breaks to the whole content
1364    /// box, and the prose around it breaks to one column. Past its
1365    /// first fragment it forbids every break, so it moves whole.
1366    #[test]
1367    fn a_spanning_block_breaks_to_the_content_box() {
1368        let css = "@page { column-count: 2; column-gap: 18pt } \
1369                   blockquote { column-span: all; margin: 0 }";
1370        let quoted = paragraph(&"a quiet sentence of prose ".repeat(18));
1371        let book = book_of(vec![section(vec![prose(), quote(vec![quoted]), prose()])]);
1372        let styles = styled(css, &book);
1373        let paginator = Paginator::new(registry(), &styles);
1374        let geometry = styles.default_page().geometry;
1375        let fragments = paginator.section_fragments(&book.sections[0]);
1376        let widest = |spanning: bool| {
1377            fragments
1378                .iter()
1379                .filter(|fragment| fragment.spanning == spanning)
1380                .filter_map(|fragment| match &fragment.piece {
1381                    Piece::Line { line, .. } => Some(fragment.x + paginator.line_width(line)),
1382                    _ => None,
1383                })
1384                .fold(0.0f32, f32::max)
1385        };
1386        assert!(widest(false) <= geometry.measure() + 1e-3);
1387        assert!(
1388            widest(true) > geometry.measure(),
1389            "the quotation broke to {} in a {} column",
1390            widest(true),
1391            geometry.measure(),
1392        );
1393        assert!(widest(true) <= geometry.content_size().0 + 1e-3);
1394
1395        let spanning: Vec<&Fragment> = fragments.iter().filter(|f| f.spanning).collect();
1396        assert!(
1397            spanning.len() > 2,
1398            "the quotation set {} lines",
1399            spanning.len()
1400        );
1401        assert!(
1402            spanning[1..]
1403                .iter()
1404                .all(|fragment| fragment.break_before == BreakPoint::Forbidden)
1405        );
1406    }
1407
1408    /// Acceptance: `h1 { position: relative; top: -12pt }` raises the
1409    /// heading and leaves the prose under it where it was.
1410    #[test]
1411    fn a_relative_heading_is_raised_and_the_prose_under_it_stays() {
1412        use crate::layout::testing::content_items;
1413        let sections = || vec![section([vec![heading("Raised")], long_prose(3)].concat())];
1414        let plain = paginate(sections());
1415        let raised = paginate_styled("h1 { position: relative; top: -12pt }", sections());
1416        assert_eq!(plain.len(), raised.len(), "the page count moved");
1417        let (plain, raised) = (content_items(&plain[0]), content_items(&raised[0]));
1418        assert_eq!(plain.len(), raised.len());
1419        let mut headings = 0;
1420        for (before, after) in plain.iter().zip(&raised) {
1421            assert_eq!(before.3, after.3, "the runs are painted in another order");
1422            assert_eq!(before.0, after.0, "{:?} moved across", after.3);
1423            if before.2 == chapter_size() {
1424                headings += 1;
1425                assert!(
1426                    (after.1 - (before.1 - 12.0)).abs() < 1e-3,
1427                    "the heading sits at {} rather than 12pt above {}",
1428                    after.1,
1429                    before.1,
1430                );
1431            } else {
1432                assert_eq!(before.1, after.1, "the prose {:?} moved", after.3);
1433            }
1434        }
1435        assert!(headings > 0, "no heading on the first page");
1436    }
1437
1438    /// Part: the engine moves a relative block and its box on every
1439    /// page the block runs over. A percentage is a percentage of the
1440    /// page area. Nothing around the block moves, and the page breaks
1441    /// do not change.
1442    #[test]
1443    fn a_relative_block_moves_its_box_and_its_lines_on_every_page() {
1444        use crate::layout::testing::{content_items, rects};
1445        let sections = || {
1446            vec![section(
1447                [
1448                    long_prose(1),
1449                    vec![quote(vec![paragraph(&"lilliputian ".repeat(500))])],
1450                    long_prose(1),
1451                ]
1452                .concat(),
1453            )]
1454        };
1455        let base = "blockquote { background-color: #eeeeee; box-decoration-break: clone }";
1456        let plain = paginate_styled(base, sections());
1457        let moved = paginate_styled(
1458            &format!("{base} blockquote {{ position: relative; left: 10%; bottom: 6pt }}"),
1459            sections(),
1460        );
1461        let (dx, dy) = (ua().default_page().geometry.content_size().0 * 0.1, -6.0);
1462        assert!(plain.len() > 1, "the quotation fits on one page");
1463        assert_eq!(plain.len(), moved.len(), "the page count moved");
1464        let near = |a: f32, b: f32| (a - b).abs() < 1e-3;
1465        let (mut boxes, mut quoted) = (0, 0);
1466        for (before, after) in plain.iter().zip(&moved) {
1467            let (was, now) = (rects(before), rects(after));
1468            assert_eq!(was.len(), now.len());
1469            for (was, now) in was.iter().zip(&now) {
1470                assert!(
1471                    near(now.0, was.0 + dx) && near(now.1, was.1 + dy),
1472                    "page {}: the box at {was:?} is painted at {now:?}",
1473                    after.number,
1474                );
1475                assert!(near(now.2, was.2) && near(now.3, was.3));
1476                boxes += 1;
1477            }
1478            for (was, now) in content_items(before).iter().zip(content_items(after)) {
1479                if was.3.contains("lilliputian") {
1480                    assert!(near(now.0, was.0 + dx) && near(now.1, was.1 + dy));
1481                    quoted += 1;
1482                } else {
1483                    assert_eq!((was.0, was.1), (now.0, now.1), "{:?} moved", now.3);
1484                }
1485            }
1486        }
1487        assert!(boxes > 1, "the box is painted on {boxes} page(s)");
1488        assert!(quoted > 0);
1489    }
1490
1491    /// Acceptance: no single line of a paragraph is stranded at a
1492    /// page boundary, either end — under the built-in sheet's two and
1493    /// two, and under an author's larger numbers.
1494    #[test]
1495    fn orphans_and_widows_hold_at_every_page_boundary() {
1496        assert_eq!((ua().root().orphans, ua().root().widows), (2, 2));
1497        assert_orphans_and_widows(&paginate(vec![section(tagged_prose(60))]), 2, 2);
1498        let pages = paginate_styled(
1499            "p { orphans: 4; widows: 3 }",
1500            vec![section(tagged_prose(60))],
1501        );
1502        assert_orphans_and_widows(&pages, 4, 3);
1503    }
1504
1505    /// The indent one level of quotation adds under the built-in
1506    /// sheet.
1507    fn quote_indent() -> f32 {
1508        let book = book_of(vec![section(vec![quote(vec![paragraph("quoted")])])]);
1509        let styles = crate::style::defaults(&book, registry());
1510        let node = styles
1511            .nodes()
1512            .iter()
1513            .find(|node| node.element == "blockquote")
1514            .expect("the sample book has a blockquote");
1515        styles.styles()[node.style as usize].margin.left
1516    }
1517
1518    /// Acceptance: a blockquote nested two deep indents twice, and
1519    /// splitting it across a page turn does not lose the indent.
1520    #[test]
1521    fn a_nested_blockquote_indents_twice_and_keeps_it_across_a_page() {
1522        let inner = vec!["inner"; 900].join(" ");
1523        let pages = paginate(vec![section(vec![
1524            paragraph(&"outside the quotation ".repeat(20)),
1525            quote(vec![
1526                paragraph(&"once removed ".repeat(20)),
1527                quote(vec![paragraph(&inner)]),
1528            ]),
1529        ])]);
1530        let indent = quote_indent();
1531        assert!(indent > 0.0, "the sheet indents nothing");
1532
1533        let mut spanned = 0;
1534        for page in &pages {
1535            let (left, _) = origin_of(page);
1536            let mut seen = false;
1537            for (_, runs) in content_lines(page) {
1538                let (x, _, text) = runs[0];
1539                if !text.starts_with("inner") {
1540                    continue;
1541                }
1542                seen = true;
1543                assert!(
1544                    (x - left - 2.0 * indent).abs() < 1e-3,
1545                    "page {}: the nested quote sits at {}, not {}",
1546                    page.number,
1547                    x - left,
1548                    2.0 * indent,
1549                );
1550            }
1551            spanned += seen as usize;
1552        }
1553        assert!(
1554            spanned >= 2,
1555            "the nested quote fitted on {spanned} page(s); nothing was split",
1556        );
1557
1558        // The measure narrows with the indent: every line of the
1559        // nested quote ends inside a box two indents narrower.
1560        let measure = master(Situation::Body(Side::Recto)).geometry.measure();
1561        for page in &pages {
1562            let (left, _) = origin_of(page);
1563            for item in &page.items {
1564                let DrawItem::Text { text, glyphs, .. } = item else {
1565                    continue;
1566                };
1567                if !text.starts_with("inner") {
1568                    continue;
1569                }
1570                for glyph in glyphs {
1571                    assert!(
1572                        glyph.x <= left + measure - indent,
1573                        "page {}: the nested quote runs past its measure",
1574                        page.number,
1575                    );
1576                }
1577            }
1578        }
1579    }
1580
1581    /// `text-indent` sinks the first line of a paragraph and leaves
1582    /// the rest of it at the full measure.
1583    #[test]
1584    fn text_indent_moves_the_first_line_and_nothing_else() {
1585        let indent = 18.0;
1586        let pages = paginate_styled(
1587            &format!("p {{ text-indent: {indent}pt; text-align: left }}"),
1588            vec![section(vec![paragraph(&"a word ".repeat(60))])],
1589        );
1590        let page = pages.first().expect("the paragraph set no pages");
1591        let (left, _) = origin_of(page);
1592        let lines = content_lines(page);
1593        assert!(
1594            lines.len() > 2,
1595            "the paragraph broke into {} line(s)",
1596            lines.len()
1597        );
1598
1599        let (_, first) = &lines[0];
1600        assert!(
1601            (first[0].0 - left - indent).abs() < 1e-3,
1602            "the first line starts at {}, not {indent} in",
1603            first[0].0 - left,
1604        );
1605        for (_, runs) in &lines[1..] {
1606            assert!(
1607                (runs[0].0 - left).abs() < 1e-3,
1608                "a later line starts at {}, not at the margin",
1609                runs[0].0 - left,
1610            );
1611        }
1612
1613        // The indent is taken out of the measure rather than hung
1614        // past it: the first line still ends inside the content box.
1615        let measure = master(Situation::Body(page.side)).geometry.measure();
1616        for item in &page.items {
1617            let DrawItem::Text { glyphs, size, .. } = item else {
1618                continue;
1619            };
1620            if *size == folio_size() {
1621                continue;
1622            }
1623            for glyph in glyphs {
1624                assert!(
1625                    glyph.x <= left + measure,
1626                    "a glyph at {} runs past the measure",
1627                    glyph.x - left,
1628                );
1629            }
1630        }
1631    }
1632
1633    /// The first-line indent the built-in sheet gives ordinary
1634    /// prose: a paragraph with another one above it.
1635    fn prose_indent() -> f32 {
1636        let book = book_of(vec![section(vec![
1637            paragraph("opening"),
1638            paragraph("following"),
1639        ])]);
1640        let styles = crate::style::defaults(&book, registry());
1641        let node = styles
1642            .nodes()
1643            .iter()
1644            .filter(|node| node.element == "p")
1645            .nth(1)
1646            .expect("the sample book has a second paragraph");
1647        styles.styles()[node.style as usize].text_indent
1648    }
1649
1650    /// The computed indent of the `nth` paragraph of a book styled by
1651    /// the built-in sheet alone.
1652    fn indent_of(sections: Vec<Section>, nth: usize) -> f32 {
1653        let book = book_of(sections);
1654        let styles = crate::style::defaults(&book, registry());
1655        let node = styles
1656            .nodes()
1657            .iter()
1658            .filter(|node| node.element == "p")
1659            .nth(nth)
1660            .expect("the book has that many paragraphs");
1661        styles.styles()[node.style as usize].text_indent
1662    }
1663
1664    /// Acceptance: the built-in sheet names the convention. A
1665    /// paragraph following another one indents; the paragraph a
1666    /// chapter opens with and the one a scene break starts again
1667    /// after do not.
1668    #[test]
1669    fn the_built_in_sheet_indents_prose_but_not_an_opening() {
1670        let indent = prose_indent();
1671        assert!(indent > 0.0, "the sheet indents nothing");
1672
1673        // Read off the tree first: an opening paragraph is flush
1674        // whether a heading or nothing at all stands above it.
1675        let words = "my father had a small estate in nottinghamshire ";
1676        let opening = vec![section(vec![
1677            heading("Chapter One"),
1678            paragraph(&words.repeat(4)),
1679            paragraph(&words.repeat(4)),
1680        ])];
1681        assert_eq!(indent_of(opening.clone(), 0), 0.0);
1682        assert_eq!(indent_of(opening, 1), indent);
1683        assert_eq!(
1684            indent_of(vec![section(vec![paragraph("alone")])], 0),
1685            0.0,
1686            "a section opening on prose indented its first paragraph",
1687        );
1688
1689        // And read it off the page: four paragraphs, the first under
1690        // a heading and the third after a scene break.
1691        let tagged = |tag: &str| paragraph(&format!("{tag} {}", words.repeat(3)));
1692        let pages = paginate(vec![section(vec![
1693            heading("Chapter One"),
1694            tagged("alpha"),
1695            tagged("bravo"),
1696            scene_break(),
1697            tagged("charlie"),
1698            tagged("delta"),
1699        ])]);
1700        let page = pages.first().expect("the chapter set no pages");
1701        let (left, _) = origin_of(page);
1702        for (tag, expected) in [
1703            ("alpha", 0.0),
1704            ("bravo", indent),
1705            ("charlie", 0.0),
1706            ("delta", indent),
1707        ] {
1708            let (_, runs) = content_lines(page)
1709                .into_iter()
1710                .find(|(_, runs)| runs[0].2.starts_with(tag))
1711                .unwrap_or_else(|| panic!("no line opens with {tag}"));
1712            assert!(
1713                (runs[0].0 - left - expected).abs() < 1e-3,
1714                "{tag} starts {}pt in, not {expected}pt",
1715                runs[0].0 - left,
1716            );
1717        }
1718    }
1719
1720    /// Acceptance: justification resolves against the shortened
1721    /// first-line measure, so an indented first line still ends
1722    /// flush on the measure's right edge.
1723    #[test]
1724    fn a_justified_first_line_still_ends_on_the_measure() {
1725        let indent = 24.0;
1726        let pages = paginate_styled(
1727            &format!("p {{ text-indent: {indent}pt; text-align: justify }}"),
1728            vec![section(vec![paragraph(
1729                &"my father had a small estate ".repeat(20),
1730            )])],
1731        );
1732        let page = pages.first().expect("the paragraph set no pages");
1733        let (left, _) = origin_of(page);
1734        let measure = master(Situation::Body(page.side)).geometry.measure();
1735        let lines = content_lines(page);
1736        assert!(lines.len() > 2, "not enough lines to justify");
1737
1738        // Every line but the last reaches the right edge, the
1739        // indented first one included: its own edge is the same edge.
1740        for (index, (baseline, _)) in lines.iter().enumerate().take(lines.len() - 1) {
1741            let right = right_edge(page, *baseline);
1742            assert!(
1743                (right - left - measure).abs() < 0.5,
1744                "line {index} ends {}pt in, not on the {measure}pt measure",
1745                right - left,
1746            );
1747        }
1748    }
1749
1750    /// Acceptance: a paragraph split over a page turn indents its
1751    /// first line and nothing else. The continuation opens flush at
1752    /// the top of the next page.
1753    #[test]
1754    fn a_paragraph_broken_across_a_page_indents_once() {
1755        let indent = 18.0;
1756        let pages = paginate_styled(
1757            &format!("p {{ text-indent: {indent}pt; text-align: left }}"),
1758            vec![section(vec![paragraph(
1759                &"my father had a small estate in nottinghamshire ".repeat(220),
1760            )])],
1761        );
1762        assert!(pages.len() > 1, "the paragraph fitted on one page");
1763        let mut indented = 0;
1764        for page in &pages {
1765            let (left, _) = origin_of(page);
1766            for (index, (_, runs)) in content_lines(page).iter().enumerate() {
1767                let start = runs[0].0 - left;
1768                if (start - indent).abs() < 1e-3 {
1769                    assert_eq!(
1770                        (page.number, index),
1771                        (pages[0].number, 0),
1772                        "page {} indented line {index}",
1773                        page.number,
1774                    );
1775                    indented += 1;
1776                } else {
1777                    assert!(
1778                        start.abs() < 1e-3,
1779                        "page {}: line {index} starts {start}pt in",
1780                        page.number,
1781                    );
1782                }
1783            }
1784        }
1785        assert_eq!(indented, 1, "the paragraph indented {indented} lines");
1786    }
1787
1788    /// Acceptance: a quotation indents from its own leading edge,
1789    /// not the page's.
1790    #[test]
1791    fn a_quotes_indent_starts_at_its_own_edge() {
1792        let indent = 9.0;
1793        let margin = quote_indent();
1794        let pages = paginate_styled(
1795            &format!("blockquote p {{ text-indent: {indent}pt; text-align: left }}"),
1796            vec![section(vec![quote(vec![paragraph(
1797                &"quoted prose runs on for a while ".repeat(12),
1798            )])])],
1799        );
1800        let page = pages.first().expect("the quote set no pages");
1801        let (left, _) = origin_of(page);
1802        let lines = content_lines(page);
1803        assert!(
1804            lines.len() > 2,
1805            "the quote broke into {} line(s)",
1806            lines.len()
1807        );
1808        assert!(
1809            (lines[0].1[0].0 - left - margin - indent).abs() < 1e-3,
1810            "the first line starts {}pt in, not {}pt",
1811            lines[0].1[0].0 - left,
1812            margin + indent,
1813        );
1814        for (_, runs) in &lines[1..] {
1815            assert!(
1816                (runs[0].0 - left - margin).abs() < 1e-3,
1817                "a later line starts {}pt in, not at the quote's edge",
1818                runs[0].0 - left,
1819            );
1820        }
1821    }
1822
1823    /// Acceptance: a scene break paints between the paragraphs it
1824    /// separates, centred in the measure, and never lands alone at a
1825    /// page boundary — neither closing a page nor opening one.
1826    #[test]
1827    fn a_scene_break_paints_between_paragraphs_and_never_lands_alone() {
1828        let mark = ornament();
1829        let words = "the drawer of knives was where it had always been and yet ";
1830        let mut blocks = Vec::new();
1831        for index in 0..24 {
1832            if index > 0 {
1833                blocks.push(scene_break());
1834            }
1835            blocks.push(paragraph(&words.repeat(3 + index % 4)));
1836        }
1837        let pages = paginate(vec![section(blocks)]);
1838
1839        let mut painted = 0;
1840        let measure = master(Situation::Body(Side::Recto)).geometry.measure();
1841        for page in &pages {
1842            let lines = content_lines(page);
1843            let marks: Vec<usize> = lines
1844                .iter()
1845                .enumerate()
1846                .filter(|(_, (_, runs))| runs[0].2 == mark)
1847                .map(|(index, _)| index)
1848                .collect();
1849            painted += marks.len();
1850            for index in marks {
1851                assert!(
1852                    index > 0,
1853                    "page {}: a scene break opened the page",
1854                    page.number,
1855                );
1856                assert!(
1857                    index + 1 < lines.len(),
1858                    "page {}: a scene break closed the page",
1859                    page.number,
1860                );
1861                let (left, _) = origin_of(page);
1862                let (x, size, text) = lines[index].1[0];
1863                let width = registry()
1864                    .shape(ua().root().font_id, text)
1865                    .unwrap_or_default()
1866                    .iter()
1867                    .map(|glyph| glyph.x_advance as f32)
1868                    .sum::<f32>()
1869                    / registry()
1870                        .metrics(ua().root().font_id)
1871                        .unwrap()
1872                        .units_per_em as f32
1873                    * size;
1874                assert!(
1875                    (x + width / 2.0 - left - measure / 2.0).abs() < 1e-3,
1876                    "page {}: the ornament is not centred in the measure",
1877                    page.number,
1878                );
1879            }
1880        }
1881        assert_eq!(painted, 23, "every scene break paints exactly once");
1882    }
1883
1884    /// Acceptance: `h1::first-line` styles the text before a hard
1885    /// break in a heading, and the text after the break takes the
1886    /// heading's own style.
1887    #[test]
1888    fn a_first_line_rule_styles_a_heading_as_far_as_its_break() {
1889        let pages = paginate_styled(
1890            "h1 { font-size: 14pt } h1::first-line { font-size: 24pt }",
1891            vec![section(vec![
1892                broken_heading("Chapter One", "The Voyage to Lilliput"),
1893                paragraph("It began."),
1894            ])],
1895        );
1896        let run = |words: &str| {
1897            pages[0]
1898                .items
1899                .iter()
1900                .find_map(|item| match item {
1901                    DrawItem::Text { text, size, y, .. }
1902                        if text.to_lowercase().contains(&words.to_lowercase()) =>
1903                    {
1904                        Some((*size, *y))
1905                    }
1906                    _ => None,
1907                })
1908                .unwrap_or_else(|| panic!("{words:?} is not drawn"))
1909        };
1910        let (opening, top) = run("Chapter One");
1911        let (rest, under) = run("The Voyage");
1912        assert_eq!(opening, 24.0, "the text before the break");
1913        assert_eq!(rest, 14.0, "the text after the break");
1914        assert!(
1915            under > top,
1916            "the text after the break is not on a line under it"
1917        );
1918    }
1919
1920    /// Acceptance: a heading with a hard break reads as one heading to
1921    /// `string-set`. The running head that prints it is one line, with
1922    /// a space where the break was.
1923    #[test]
1924    fn a_heading_with_a_break_sets_one_running_head() {
1925        let pages = paginate_styled(
1926            "h1 { string-set: chapter content() }
1927             @page :left { @top-left { content: string(chapter) } }
1928             @page :right { @top-right { content: string(chapter) } }",
1929            vec![section(vec![
1930                broken_heading("Chapter One", "The Voyage to Lilliput"),
1931                paragraph(&"my father had a small estate in nottinghamshire ".repeat(200)),
1932            ])],
1933        );
1934        assert!(pages.len() > 1, "the chapter fits on one page");
1935        let head: Vec<(String, f32)> = pages[1]
1936            .items
1937            .iter()
1938            .filter_map(|item| match item {
1939                DrawItem::Text { text, y, .. } => Some((text.to_lowercase(), *y)),
1940                _ => None,
1941            })
1942            .filter(|(text, _)| text.contains("chapter") || text.contains("voyage"))
1943            .collect();
1944        assert!(!head.is_empty(), "page 2 has no running head");
1945        assert!(
1946            head.iter().all(|(_, y)| *y == head[0].1),
1947            "the running head is not one line: {head:?}",
1948        );
1949        let printed: String = head.iter().map(|(text, _)| text.as_str()).collect();
1950        assert_eq!(printed, "chapter one the voyage to lilliput");
1951    }
1952
1953    /// Acceptance: `h3 + p::first-line { font-variant-caps:
1954    /// small-caps }` draws the opening line in small capitals and the
1955    /// rest of the paragraph in the letters the author wrote. The
1956    /// change stops at the break the paragraph came to: the last run
1957    /// of the first line is small capitals and the first run of the
1958    /// second is not.
1959    #[test]
1960    fn a_first_line_of_small_capitals_stops_where_the_line_does() {
1961        let prose = "my father had a small estate in nottinghamshire ".repeat(6);
1962        let pages = paginate_styled(
1963            "h3 + p::first-line { font-variant-caps: small-caps }",
1964            under_h3(&prose),
1965        );
1966        let lines = small_caps_lines(&pages[0]);
1967        assert!(lines.len() > 3, "not enough lines to break");
1968
1969        // The heading is the first baseline; the paragraph follows.
1970        let opening = &lines[1].1;
1971        let next = &lines[2].1;
1972        assert!(
1973            opening.iter().all(|(_, small)| *small),
1974            "the opening line is not all small capitals: {opening:?}",
1975        );
1976        assert!(
1977            next.iter().all(|(_, small)| !*small),
1978            "the small capitals ran past the first line: {next:?}",
1979        );
1980        assert!(
1981            lines[3..]
1982                .iter()
1983                .all(|(_, runs)| runs.iter().all(|(_, small)| !*small)),
1984            "the small capitals reached further down the page",
1985        );
1986
1987        // The letters are the ones the author wrote: the face draws
1988        // the capitals, the text does not spell them.
1989        // A break swallows the space it falls on, so the lines join
1990        // back with one between them.
1991        let set = lines[1..]
1992            .iter()
1993            .map(|(_, runs)| runs.iter().map(|(text, _)| *text).collect::<String>())
1994            .collect::<Vec<_>>()
1995            .join(" ");
1996        assert!(
1997            prose.starts_with(set.trim_end()),
1998            "the paragraph was not set as it was written: {set:?}",
1999        );
2000    }
2001
2002    /// Acceptance: an image warning names its url once, so the reader
2003    /// does not read the same name twice on one line.
2004    #[test]
2005    fn an_image_warning_names_its_url_once() {
2006        struct Tall;
2007        impl crate::images::ImageLoader for Tall {
2008            fn load(&self, url: &str) -> Option<Vec<u8>> {
2009                (url == "tall.png").then(|| png(768, 1536))
2010            }
2011        }
2012
2013        let book = book_of(vec![section(vec![Block::Image {
2014            id: NodeId::UNASSIGNED,
2015            url: "tall.png".into(),
2016            alt: "a drawer of knives".into(),
2017            attributes: Attributes::default(),
2018            position: Some(SourcePos { line: 9, column: 1 }),
2019            span: None,
2020        }])]);
2021        let styles = crate::style::defaults(&book, registry());
2022        let assets = crate::images::Assets::probe(&book, &styles, &Tall);
2023        let output = layout_book(&book, &styles, registry(), &assets);
2024
2025        let warning = output
2026            .warnings
2027            .iter()
2028            .find(|warning| warning.message.contains("tall.png"))
2029            .expect("scaling an image to fit is worth saying");
2030        assert_eq!(warning.message.matches("tall.png").count(), 1);
2031        assert!(warning.message.ends_with('.'), "{}", warning.message);
2032        assert!(!warning.message.contains(';'), "{}", warning.message);
2033    }
2034
2035    /// Acceptance: an image taller than the content box is scaled to
2036    /// fit it, keeping its ratio, and the run says so.
2037    #[test]
2038    fn an_image_taller_than_the_content_box_scales_and_warns() {
2039        struct Png;
2040        impl crate::images::ImageLoader for Png {
2041            fn load(&self, url: &str) -> Option<Vec<u8>> {
2042                match url {
2043                    // 8in by 16in at 96dpi: taller than the page.
2044                    "tall.png" => Some(png(768, 1536)),
2045                    // 2in square: it fits as it is.
2046                    "small.png" => Some(png(192, 192)),
2047                    _ => None,
2048                }
2049            }
2050        }
2051
2052        let image = |url: &str| Block::Image {
2053            id: NodeId::UNASSIGNED,
2054            url: url.into(),
2055            alt: "a drawer of knives".into(),
2056            attributes: Attributes::default(),
2057            position: Some(SourcePos { line: 9, column: 1 }),
2058            span: None,
2059        };
2060        let book = book_of(vec![section(vec![
2061            paragraph("before"),
2062            image("tall.png"),
2063            image("small.png"),
2064        ])]);
2065        let styles = crate::style::defaults(&book, registry());
2066        let assets = crate::images::Assets::probe(&book, &styles, &Png);
2067        let output = layout_book(&book, &styles, registry(), &assets);
2068
2069        let placed: Vec<(f32, f32, u32)> = output
2070            .pages
2071            .iter()
2072            .flat_map(|page| page.items.iter())
2073            .filter_map(|item| match item {
2074                DrawItem::Image { w, h, asset, .. } => Some((*w, *h, *asset)),
2075                _ => None,
2076            })
2077            .collect();
2078        assert_eq!(placed.len(), 2, "both images are placed");
2079
2080        let (_, height) = master(Situation::Body(Side::Recto)).geometry.content_size();
2081        let (width, tall, asset) = placed[0];
2082        assert_eq!(asset, 0, "the first image indexes the first asset");
2083        assert!(
2084            (tall - height).abs() < 1e-3,
2085            "the tall image is {tall}pt in a {height}pt box",
2086        );
2087        assert!(
2088            (width / tall - 0.5).abs() < 1e-3,
2089            "scaling did not keep the ratio: {width} by {tall}",
2090        );
2091        // The one that fits keeps its intrinsic size: 2in square.
2092        assert_eq!(placed[1], (144.0, 144.0, 1));
2093
2094        let warning = output
2095            .warnings
2096            .iter()
2097            .find(|warning| warning.message.contains("tall.png"))
2098            .expect("scaling an image to fit is worth saying");
2099        assert!(warning.message.contains("is taller than the page"));
2100        assert_eq!(warning.origin.as_deref(), Some("9:1"));
2101        assert!(
2102            !output
2103                .warnings
2104                .iter()
2105                .any(|w| w.message.contains("small.png")),
2106            "an image that fits is not worth a diagnostic",
2107        );
2108    }
2109
2110    /// `break-before` and `break-after` reach fragmentation from the
2111    /// cascade, and nothing in the paginator hardcodes them: a sheet
2112    /// that turns the recto rule off runs the chapters together, and
2113    /// one that asks for a page break gets one.
2114    #[test]
2115    fn break_control_comes_from_the_cascade() {
2116        let chapters = || {
2117            vec![
2118                section(vec![heading("One"), paragraph("The first chapter.")]),
2119                section(vec![heading("Two"), paragraph("The second chapter.")]),
2120            ]
2121        };
2122        // The built-in sheet opens a chapter on a recto.
2123        assert_eq!(paginate(chapters()).len(), 3);
2124        // The author turns that off and the chapters run together.
2125        assert_eq!(
2126            paginate_styled("section { break-before: auto }", chapters()).len(),
2127            1,
2128        );
2129        // A page break, without a side, is still a page break.
2130        assert_eq!(
2131            paginate_styled("section { break-before: page }", chapters()).len(),
2132            2,
2133        );
2134        // And a verso open leaves the blank recto behind it.
2135        let pages = paginate_styled("section { break-before: verso }", chapters());
2136        assert_eq!(pages.len(), 4);
2137        assert!(pages[0].items.is_empty() || pages[2].items.is_empty());
2138    }
2139
2140    /// `break-inside: avoid` moves a block whole rather than split
2141    /// it, and `break-after: avoid` keeps a heading with the prose
2142    /// under it.
2143    #[test]
2144    fn avoid_keeps_blocks_and_headings_with_what_follows_them() {
2145        // A page of single-line paragraphs, all but a few lines
2146        // full, and then a quotation too long for what is left.
2147        let quoted = "quoted words that would rather not be split across a page turn ";
2148        let filler: Vec<Block> = (0..30)
2149            .map(|index| paragraph(&format!("filler line {index}")))
2150            .collect();
2151        let blocks = [
2152            filler,
2153            vec![quote(vec![paragraph(&quoted.repeat(6))])],
2154            long_prose(2),
2155        ]
2156        .concat();
2157        // A quoted line is one set at the quotation's indent: only
2158        // its first begins with the words the quotation opens with.
2159        let indent = quote_indent();
2160        let split = |css: &str| {
2161            let pages = paginate_styled(css, vec![section(blocks.clone())]);
2162            pages
2163                .iter()
2164                .filter(|page| {
2165                    let (left, _) = origin_of(page);
2166                    content_lines(page)
2167                        .iter()
2168                        .any(|(_, runs)| (runs[0].0 - left - indent).abs() < 1e-3)
2169                })
2170                .count()
2171        };
2172        assert!(split("") >= 2, "the quotation should straddle a page");
2173        assert_eq!(
2174            split("blockquote { break-inside: avoid }"),
2175            1,
2176            "an avoided blockquote should move whole",
2177        );
2178
2179        // A heading is never the last thing on a page: the built-in
2180        // sheet gives it `break-after: avoid`.
2181        let pages = paginate(vec![
2182            section(long_prose(12)),
2183            section([vec![heading("Two")], long_prose(12)].concat()),
2184        ]);
2185        for page in &pages {
2186            let lines = content_lines(page);
2187            if let Some((_, runs)) = lines.last() {
2188                assert!(
2189                    runs[0].1 != chapter_size(),
2190                    "page {}: a heading closed the page",
2191                    page.number,
2192                );
2193            }
2194        }
2195    }
2196
2197    /// An `h4` with the words `value`.
2198    fn h4(value: &str) -> Block {
2199        let Block::Heading { inlines, .. } = heading(value) else {
2200            unreachable!("`heading` makes a heading");
2201        };
2202        Block::Heading {
2203            id: NodeId::UNASSIGNED,
2204            level: HeadingLevel::H4,
2205            inlines,
2206            attributes: Attributes::default(),
2207            position: None,
2208            span: None,
2209        }
2210    }
2211
2212    /// Each content line of a page as the text of its runs, with its
2213    /// baseline, its leading edge and its size.
2214    fn read_lines(page: &Page) -> Vec<(f32, f32, f32, String)> {
2215        content_lines(page)
2216            .into_iter()
2217            .map(|(baseline, runs)| {
2218                let text = runs.iter().map(|run| run.2).collect();
2219                (baseline, runs[0].0, runs[0].1, text)
2220            })
2221            .collect()
2222    }
2223
2224    /// The width of a line of text on the opening page of a chapter.
2225    fn opening_measure() -> f32 {
2226        master(Situation::First(Side::Recto)).geometry.measure()
2227    }
2228
2229    /// Part: a generated box holds the value of `content`, which is a
2230    /// string or nothing.
2231    #[test]
2232    fn a_generated_box_holds_its_string_or_nothing() {
2233        let blocks = || vec![section(vec![paragraph("the paragraph itself")])];
2234        let said = paginate_styled("p::before { content: \"Before it\" }", blocks());
2235        let texts: Vec<String> = read_lines(&said[0])
2236            .into_iter()
2237            .map(|line| line.3)
2238            .collect();
2239        assert_eq!(texts, ["Before it", "the paragraph itself"]);
2240
2241        let bare = paginate(blocks());
2242        let empty = paginate_styled("p::before { content: \"\" }", blocks());
2243        assert_eq!(read_lines(&empty[0]), read_lines(&bare[0]));
2244    }
2245
2246    /// Part: the generated box is a block in the flow, the first or
2247    /// the last child of its element, across the element's content box
2248    /// and in the element's style.
2249    #[test]
2250    fn a_generated_box_is_the_first_or_last_child_of_its_block() {
2251        let pages = paginate_styled(
2252            "blockquote { font-size: 10pt; margin: 0 20pt } \
2253             blockquote::before { content: \"first\" } \
2254             blockquote::after { content: \"last\" }",
2255            vec![section(vec![
2256                quote(vec![paragraph("inside one"), paragraph("inside two")]),
2257                paragraph("outside"),
2258            ])],
2259        );
2260        let lines = read_lines(&pages[0]);
2261        let texts: Vec<&str> = lines.iter().map(|line| line.3.as_str()).collect();
2262        assert_eq!(
2263            texts,
2264            ["first", "inside one", "inside two", "last", "outside"]
2265        );
2266        let (left, _) = origin_of(&pages[0]);
2267        for (_, x, size, text) in [&lines[0], &lines[3]] {
2268            assert!((x - (left + 20.0)).abs() < 1e-3, "{text} starts at {x}");
2269            assert_eq!(*size, 10.0, "{text} is not in the quotation's size");
2270        }
2271    }
2272
2273    /// Part: the generated box takes the box model. Its border box
2274    /// holds its border, its padding and its height, inside its own
2275    /// margins.
2276    #[test]
2277    fn a_generated_box_takes_the_box_model() {
2278        const TINT: Color = Color::rgb(0xee, 0xee, 0xee);
2279        const INK: Color = Color::rgb(0xd6, 0x07, 0x5e);
2280        let blocks = || vec![section(vec![heading("Title"), paragraph("after")])];
2281        let pages = paginate_styled(
2282            "h1::after { content: \"\"; height: 2pt; padding: 3pt 0; \
2283             border-top: 1pt solid #d6075e; margin: 0 12pt; background-color: #eeeeee }",
2284            blocks(),
2285        );
2286        let (left, _) = origin_of(&pages[0]);
2287        let measure = opening_measure();
2288        let boxes = rects(&pages[0]);
2289        let [(x, _, w, h, _)] = boxes
2290            .iter()
2291            .filter(|rect| rect.4 == TINT)
2292            .collect::<Vec<_>>()[..]
2293        else {
2294            panic!("one background: {boxes:?}");
2295        };
2296        assert!((x - (left + 12.0)).abs() < 1e-3, "the box starts at {x}");
2297        assert!((w - (measure - 24.0)).abs() < 1e-3, "the box is {w} wide");
2298        assert!((h - 9.0).abs() < 1e-3, "the box is {h} tall");
2299        assert!(
2300            boxes
2301                .iter()
2302                .any(|rect| rect.4 == INK && (rect.3 - 1.0).abs() < 1e-3),
2303            "no top border: {boxes:?}",
2304        );
2305        let moved = baseline_of(&pages, "after") - baseline_of(&paginate(blocks()), "after");
2306        assert!((moved - 9.0).abs() < 1e-3, "the prose moved {moved}");
2307    }
2308
2309    /// Acceptance: `h4::after { content: ""; height: 2pt;
2310    /// background-color: #d6075e }` draws a rule under every h4, at
2311    /// the width of the heading's content box.
2312    #[test]
2313    fn a_generated_rule_sits_under_every_h4_across_its_content_box() {
2314        const INK: Color = Color::rgb(0xd6, 0x07, 0x5e);
2315        let pages = paginate_styled(
2316            "h4 { margin: 0 20pt; padding: 0 10pt } \
2317             h4::after { content: \"\"; height: 2pt; background-color: #d6075e }",
2318            vec![section(vec![
2319                h4("First"),
2320                paragraph("one"),
2321                h4("Second"),
2322                paragraph("two"),
2323            ])],
2324        );
2325        let page = &pages[0];
2326        let (left, _) = origin_of(page);
2327        let measure = opening_measure();
2328        let lines = read_lines(page);
2329        let rules: Vec<_> = rects(page)
2330            .into_iter()
2331            .filter(|rect| rect.4 == INK)
2332            .collect();
2333        assert_eq!(rules.len(), 2, "{rules:?}");
2334        for ((x, y, w, h, _), title) in rules.iter().zip(["First", "Second"]) {
2335            assert!(
2336                (x - (left + 30.0)).abs() < 1e-3,
2337                "{title}: the rule starts at {x}"
2338            );
2339            assert!(
2340                (w - (measure - 60.0)).abs() < 1e-3,
2341                "{title}: the rule is {w} wide"
2342            );
2343            assert!((h - 2.0).abs() < 1e-3, "{title}: the rule is {h} tall");
2344            let at = lines
2345                .iter()
2346                .position(|line| line.3 == title)
2347                .expect("the heading is set");
2348            assert!(*y > lines[at].0, "{title}: the rule is above the baseline");
2349            assert!(
2350                y + h <= lines[at + 1].0,
2351                "{title}: the rule runs into the prose"
2352            );
2353        }
2354    }
2355
2356    /// Acceptance: `blockquote::before { content: "\201C" }` sets an
2357    /// opening quotation mark before the quote's first line.
2358    #[test]
2359    fn a_generated_quotation_mark_opens_the_quotation() {
2360        let pages = paginate_styled(
2361            "blockquote::before { content: \"\\201C\" }",
2362            vec![section(vec![quote(vec![paragraph("quoted words")])])],
2363        );
2364        let lines = read_lines(&pages[0]);
2365        let texts: Vec<&str> = lines.iter().map(|line| line.3.as_str()).collect();
2366        assert_eq!(texts, ["\u{201C}", "quoted words"]);
2367        let (left, _) = origin_of(&pages[0]);
2368        assert!(
2369            (lines[0].1 - (left + quote_indent())).abs() < 1e-3,
2370            "the mark starts at {}",
2371            lines[0].1,
2372        );
2373    }
2374
2375    /// Acceptance: a generated box takes height in the flow, and the
2376    /// block under it moves down by that height.
2377    #[test]
2378    fn the_block_under_a_generated_box_moves_down_by_its_height() {
2379        let blocks = || vec![section(vec![paragraph("one"), paragraph("two")])];
2380        let bare = paginate(blocks());
2381        let tall = paginate_styled("p::after { content: \"\"; height: 30pt }", blocks());
2382        let moved = baseline_of(&tall, "two") - baseline_of(&bare, "two");
2383        assert!((moved - 30.0).abs() < 1e-3, "the paragraph moved {moved}");
2384    }
2385
2386    /// Acceptance: a page break falling on a generated box keeps it
2387    /// with the element it belongs to. No page opens between a box and
2388    /// its element, so no page opens on a box that `::after` made.
2389    #[test]
2390    fn a_page_break_keeps_a_generated_box_with_its_element() {
2391        let book = book_of(vec![section(vec![paragraph(&"words ".repeat(60))])]);
2392        let fragments = |css: &str| {
2393            let styles = styled(css, &book);
2394            Paginator::new(registry(), &styles).section_fragments(&book.sections[0])
2395        };
2396        let lines = |fragments: &[Fragment]| -> Vec<usize> {
2397            fragments
2398                .iter()
2399                .enumerate()
2400                .filter(|(_, fragment)| matches!(fragment.piece, Piece::Line { .. }))
2401                .map(|(index, _)| index)
2402                .collect()
2403        };
2404        let bare = fragments("");
2405        assert_ne!(bare[lines(&bare)[0]].break_before, BreakPoint::Forbidden);
2406
2407        let boxed =
2408            fragments("p::before { content: \"x\" } p::after { content: \"\"; height: 20pt }");
2409        let at = lines(&boxed);
2410        assert_eq!(
2411            boxed[at[1]].break_before,
2412            BreakPoint::Forbidden,
2413            "the paragraph can leave the box before it",
2414        );
2415        let after = at.last().expect("the paragraph has lines") + 1;
2416        assert!(
2417            after < boxed.len(),
2418            "the box after the paragraph is missing"
2419        );
2420        for fragment in &boxed[after..] {
2421            assert_eq!(
2422                fragment.break_before,
2423                BreakPoint::Forbidden,
2424                "the box after the paragraph can leave it",
2425            );
2426        }
2427
2428        let pages = paginate_styled(
2429            "p { orphans: 1; widows: 1 } \
2430             p::after { content: \"\"; height: 24pt; background-color: #eeeeee }",
2431            vec![section(long_prose(40))],
2432        );
2433        assert!(pages.len() > 2, "the prose fills {} pages", pages.len());
2434        for page in &pages {
2435            let lines = content_lines(page);
2436            for (_, y, ..) in rects(page) {
2437                assert!(
2438                    lines.first().is_some_and(|(baseline, _)| *baseline < y),
2439                    "page {}: a generated box opens the page",
2440                    page.number,
2441                );
2442            }
2443        }
2444    }
2445}