1use 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 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, §ion.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
66pub(super) struct Builder<'a, 'p> {
69 pub(super) paginator: &'p Paginator<'a>,
70 pub(super) source: Option<&'p str>,
72 pub(super) fragments: Vec<Fragment>,
73 pub(super) pending: BreakPoint,
75 pub(super) margin: f32,
78 pub(super) fixed: f32,
81 pub(super) pending_marks: Option<Box<Marks>>,
84 open: Vec<Pending>,
86 spanning: bool,
88 pub(super) layer: i32,
92 offset: (f32, f32),
95 moved: Vec<(f32, f32)>,
98 pub(super) opacity: f32,
102 faded: Vec<f32>,
105 pub(super) lifted: Option<NodeId>,
108 depth: f32,
111 tall: Vec<Tall>,
113}
114
115impl<'a, 'p> Builder<'a, 'p> {
116 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#[derive(Debug, Clone, Copy)]
144pub(super) enum Child<'b> {
145 Block(&'b Block),
146 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
159pub(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
179struct Tall {
182 top: f32,
184 definite: Option<f32>,
187 least: f32,
189}
190
191struct Pending {
193 start: usize,
195 open_fixed: f32,
198 started: bool,
203 decoration: Decoration,
204}
205
206impl Builder<'_, '_> {
207 fn styles(&self) -> &StyleTree {
208 self.paginator.styles
209 }
210
211 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 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 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 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 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 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 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 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 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 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 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 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 decoration.below = self.fixed;
458 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 fn emit_one(&mut self, x: f32, height: f32, piece: Piece) {
474 self.emit(&mut true, Fragment::plain(x, height, piece));
475 }
476
477 pub(super) fn emit(&mut self, first: &mut bool, mut fragment: Fragment) {
482 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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, ¶graph).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(¶graph).height(),
926 Piece::Blank,
927 )
928 });
929 self.emit_one(x + offset, height, piece);
930 }
931
932 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
971pub(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 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 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
1052pub(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#[derive(Debug, Clone)]
1102pub(super) struct Setting {
1103 pub(super) x: f32,
1105 align: TextAlign,
1106 orphans: usize,
1107 widows: usize,
1108 pub(super) cap: Option<Cap>,
1110 cap_x: f32,
1113 notes: Vec<Arc<Note>>,
1117}
1118
1119impl Setting {
1120 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#[derive(Debug)]
1143pub struct Reflow {
1144 pub(super) shaped: Shaped,
1145 pub(super) setting: Setting,
1146 pub(super) base: Measure,
1149 pub(super) leading: f32,
1151 pub(super) ends: Vec<usize>,
1154}
1155
1156pub(super) struct Stacked {
1158 pub(super) items: Vec<DrawItem>,
1160 pub(super) boxes: Vec<(NodeId, PageBox)>,
1162 pub(super) height: f32,
1164 pub(super) anchors: Vec<NodeId>,
1166 pub(super) notes: Vec<Arc<Note>>,
1169 pub(super) marks: Option<Box<Marks>>,
1172}
1173
1174pub(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 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
1227pub(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
1238fn 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 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 #[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 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 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 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 #[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 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 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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 #[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 #[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 "tall.png" => Some(png(768, 1536)),
2045 "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 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 #[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 assert_eq!(paginate(chapters()).len(), 3);
2124 assert_eq!(
2126 paginate_styled("section { break-before: auto }", chapters()).len(),
2127 1,
2128 );
2129 assert_eq!(
2131 paginate_styled("section { break-before: page }", chapters()).len(),
2132 2,
2133 );
2134 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 #[test]
2144 fn avoid_keeps_blocks_and_headings_with_what_follows_them() {
2145 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("ed.repeat(6))])],
2154 long_prose(2),
2155 ]
2156 .concat();
2157 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 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 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 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 fn opening_measure() -> f32 {
2226 master(Situation::First(Side::Recto)).geometry.measure()
2227 }
2228
2229 #[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 #[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 #[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 #[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 #[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 #[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 #[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}