Skip to main content

retex_engine/
pagenote.rs

1//! Explanations for underfull pages, for the default output mode.
2//!
3//! TeX's message "Underfull \vbox (badness N) has occurred while \output is
4//! active" says only that the output routine packed a page whose glue had
5//! to stretch too much. The usual cause is what did *not* go on the page:
6//! the page builder broke the page early because the next item (a figure,
7//! a heading kept with its paragraph, lines held back by the widow and club
8//! penalties) did not fit, or because a `\newpage` or `\clearpage` forced
9//! the break. The page builder records the situation at each page break
10//! ([`PageBreakInfo`]); the diagnostic explains it in those terms.
11
12use crate::Engine;
13use crate::nodes::{NodeData, NodeRef, id};
14
15/// What the page builder knew when it broke a page.
16#[derive(Clone, Debug, Default, PartialEq)]
17pub struct PageBreakInfo {
18    /// The page number (`\count0`).
19    pub page: i32,
20    /// The page goal (the text height, less insertions).
21    pub goal: i32,
22    /// The natural height of the page material.
23    pub total: i32,
24    /// Its finite stretch.
25    pub stretch: i32,
26    /// Whether it had infinite stretch (`\vfil` and the like).
27    pub infinite: bool,
28    /// Whether the break was forced (penalty ≤ -10000).
29    pub forced: bool,
30    /// The first box that went to the next page.
31    pub moved: Option<Moved>,
32}
33
34/// A box moved to the next page.
35#[derive(Clone, Debug, PartialEq)]
36pub struct Moved {
37    /// Its height plus depth.
38    pub height: i32,
39    /// What it is.
40    pub kind: MovedKind,
41}
42
43/// The kind of a [`Moved`] box.
44#[derive(Clone, Debug, PartialEq)]
45pub enum MovedKind {
46    /// A line of a paragraph, with the start of its text.
47    Line(String),
48    /// A box with an image.
49    Image,
50    /// Another box (a table, a minipage, a display…).
51    Box,
52}
53
54/// Points, with two decimals.
55fn pt(sp: i32) -> String {
56    format!("{:.2}pt", f64::from(sp) / 65536.0)
57}
58
59impl Engine {
60    /// The text at the start of list `p` (glyphs, with spaces for glue),
61    /// at most `max` characters.
62    fn list_excerpt(&self, p: NodeRef, out: &mut String, max: usize) {
63        let mut p = p;
64        while !p.is_null() && out.chars().count() < max {
65            match &self.nodes.get(p).data {
66                NodeData::Glyph(g) => {
67                    if let Some(c) = char::from_u32(g.chr) {
68                        out.push(c);
69                    }
70                }
71                NodeData::Glue { .. } if !out.is_empty() && !out.ends_with(' ') => out.push(' '),
72                NodeData::Box(b) => self.list_excerpt(b.head, out, max),
73                _ => {}
74            }
75            p = self.nodes.next(p);
76        }
77    }
78
79    /// Whether list `p` holds an image (at any depth).
80    fn has_image(&self, mut p: NodeRef) -> bool {
81        while !p.is_null() {
82            if self.is_image(p) {
83                return true;
84            }
85            if let NodeData::Box(b) = &self.nodes.get(p).data
86                && self.has_image(b.head)
87            {
88                return true;
89            }
90            p = self.nodes.next(p);
91        }
92        false
93    }
94
95    /// The first box after `p` in its list.
96    pub(crate) fn first_moved(&self, p: NodeRef) -> Option<Moved> {
97        let mut q = if p.is_null() { p } else { self.nodes.next(p) };
98        while !q.is_null() {
99            let t = self.nodes.id(q);
100            if t == id::HLIST || t == id::VLIST {
101                let b = self.nodes.box_data(q)?;
102                let kind = if self.has_image(b.head) {
103                    MovedKind::Image
104                } else if t == id::HLIST {
105                    let mut s = String::new();
106                    self.list_excerpt(b.head, &mut s, 50);
107                    if s.trim().is_empty() {
108                        MovedKind::Box
109                    } else {
110                        MovedKind::Line(s.trim().to_owned())
111                    }
112                } else {
113                    MovedKind::Box
114                };
115                return Some(Moved {
116                    height: b.height + b.depth,
117                    kind,
118                });
119            }
120            if t == id::RULE {
121                return None;
122            }
123            q = self.nodes.next(q);
124        }
125        None
126    }
127
128    /// The message and help lines for an underfull page, from the last
129    /// page break.
130    /// Returns the message, the notes (causes) and the help (suggestions).
131    pub fn underfull_page_note(&self, badness: i32) -> Option<(String, Vec<String>, Vec<String>)> {
132        let info = self.page.last_break.as_ref()?;
133        let short = info.goal - info.total;
134        let message = format!(
135            "Page {} is {} shorter than the text height, and its vertical spaces had to \
136             stretch too much to fill it (badness {badness})",
137            info.page,
138            pt(short.max(0))
139        );
140        let mut notes = Vec::new();
141        let mut help = Vec::new();
142        if info.forced {
143            notes.push(
144                "the page was ended early by a forced page break (\\newpage, \\clearpage, \
145                 \\pagebreak, the end of a chapter or a float page)"
146                    .to_owned(),
147            );
148        } else if let Some(m) = &info.moved {
149            let what = match &m.kind {
150                MovedKind::Line(text) => format!("the line starting “{text}…”"),
151                MovedKind::Image => "a box with an image".to_owned(),
152                MovedKind::Box => "a box (a table, a minipage or similar)".to_owned(),
153            };
154            notes.push(format!(
155                "the next item, {what} ({} tall), did not fit in the remaining space, so the page \
156                 was broken before it",
157                pt(m.height)
158            ));
159            if matches!(m.kind, MovedKind::Line(_)) {
160                notes.push(
161                    "TeX may also have moved lines to avoid a widow or club line (a single \
162                     line of a paragraph alone at the top or bottom of a page), or to keep a \
163                     heading with the text that follows it"
164                        .to_owned(),
165                );
166            } else {
167                help.push(
168                    "consider making it smaller, or placing it in a float (figure or table \
169                     environment) that LaTeX can move to a better place"
170                        .to_owned(),
171                );
172            }
173        }
174        notes.push(format!(
175            "the page's spaces could stretch by {} without becoming too loose",
176            pt(info.stretch)
177        ));
178        help.push(
179            "\\raggedbottom in the preamble lets pages end short instead of stretching their \
180             spaces (this warning then disappears)"
181                .to_owned(),
182        );
183        Some((message, notes, help))
184    }
185}