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