1use crate::Engine;
13use crate::nodes::{NodeData, NodeRef, id};
14
15#[derive(Clone, Debug, Default, PartialEq)]
17pub struct PageBreakInfo {
18 pub page: i32,
20 pub goal: i32,
22 pub total: i32,
24 pub stretch: i32,
26 pub infinite: bool,
28 pub forced: bool,
30 pub moved: Option<Moved>,
32}
33
34#[derive(Clone, Debug, PartialEq)]
36pub struct Moved {
37 pub height: i32,
39 pub kind: MovedKind,
41}
42
43#[derive(Clone, Debug, PartialEq)]
45pub enum MovedKind {
46 Line(String),
48 Image,
50 Box,
52}
53
54fn pt(sp: i32) -> String {
56 format!("{:.2}pt", f64::from(sp) / 65536.0)
57}
58
59impl Engine {
60 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 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 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 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}