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
//! Facts about over- and underfull hboxes, for the default output mode.
//!
//! TeX's "Overfull \hbox (12pt too wide) in paragraph at lines 3--5" says
//! neither which line nor what sticks out. When the box is reported, the
//! engine records its text and, for an overfull box, the word that crosses
//! its right edge; the driver turns them into a message that names the word
//! and suggests a fix (a URL wants `\url`, a long word hyphenation hints).
use crate::Engine;
use crate::nodes::{NodeData, NodeRef, glyph_sub};
/// What an hbox contains, read from its list.
#[derive(Default)]
struct Reader {
/// The text, with glue as spaces.
text: String,
/// The word being read and where it starts.
word: String,
/// Whether the right edge is crossed in a box or rule of the word.
word_box: bool,
/// The horizontal position.
x: i64,
/// The box's width.
width: i64,
/// The word that crosses the right edge, once found, and whether a box
/// or rule in it does.
overflow: Option<(String, bool)>,
/// Whether the position passed the edge inside the current word.
crossed: bool,
}
impl Reader {
fn end_word(&mut self) {
if self.crossed && self.overflow.is_none() {
self.overflow = Some((std::mem::take(&mut self.word), self.word_box));
}
self.word.clear();
self.word_box = false;
self.crossed = false;
}
fn advance(&mut self, w: i32) {
self.x += i64::from(w);
if self.x > self.width {
self.crossed = true;
}
}
/// Advances over a box or rule: the word crosses the edge in it.
fn advance_box(&mut self, w: i32) {
let before = self.crossed;
self.advance(w);
if self.crossed && !before {
self.word_box = true;
}
}
}
impl Engine {
/// Facts about the hbox `r` just reported as over- or underfull:
/// `text` (its text), `overflow` (the word crossing the right edge),
/// `overflow_box` (`1` if the edge is crossed in a box or rule: an image,
/// a table, an `\mbox`), `width` (the
/// box's width in pt).
pub(crate) fn hbox_facts(&self, r: NodeRef) -> Vec<(String, String)> {
let NodeData::Box(b) = self.nodes.data(r) else {
return Vec::new();
};
let (sign, order, set) = (b.glue_sign, b.glue_order, b.glue_set);
let mut rd = Reader {
width: i64::from(b.width),
..Reader::default()
};
let mut p = b.head;
while !p.is_null() {
match self.nodes.data(p) {
NodeData::Glue { spec, .. } => {
let mut w = spec.width;
if sign == 1 && spec.stretch_order == order {
w += (set * f64::from(spec.stretch)).round() as i32;
} else if sign == 2 && spec.shrink_order == order {
w -= (set * f64::from(spec.shrink)).round() as i32;
}
rd.end_word();
rd.advance(w);
if !rd.text.ends_with(' ') {
rd.text.push(' ');
}
}
NodeData::Glyph(_) => {
let mut s = String::new();
self.glyph_text(p, &mut s);
rd.text.push_str(&s);
rd.word.push_str(&s);
rd.advance(self.glyph_dimensions(p).0);
}
NodeData::Box(inner) => {
let mut s = String::new();
self.list_text(inner.head, &mut s);
let s = s.trim().to_owned();
rd.text.push_str(&s);
rd.word.push_str(&s);
rd.advance_box(inner.width);
}
NodeData::Rule { width, .. } => {
if *width > 0 {
rd.advance_box(*width);
}
}
NodeData::Disc { replace, .. } => {
let mut s = String::new();
self.list_text(*replace, &mut s);
rd.text.push_str(&s);
rd.word.push_str(&s);
rd.advance(self.natural_dimensions(*replace, NodeRef::NULL).0);
}
NodeData::Kern { kern, .. } => rd.advance(*kern),
NodeData::Math { surround, .. } => rd.advance(*surround),
_ => {}
}
p = self.nodes.next(p);
}
rd.end_word();
let mut facts = vec![
("text".to_owned(), rd.text.trim().to_owned()),
(
"width".to_owned(),
format!("{:.1}", f64::from(b.width) / 65536.0),
),
];
if let Some((word, is_box)) = rd.overflow {
facts.push(("overflow".to_owned(), word));
if is_box {
facts.push(("overflow_box".to_owned(), "1".to_owned()));
}
}
facts
}
/// Appends the characters of glyph `p` (the components of a ligature).
fn glyph_text(&self, p: NodeRef, out: &mut String) {
let n = self.nodes.get(p);
if let NodeData::Glyph(g) = &n.data {
if n.subtype & glyph_sub::LIGATURE != 0 && !g.components.is_null() {
self.list_text(g.components, out);
} else if let Some(c) =
char::from_u32(g.chr).filter(|c| !matches!(*c as u32, 0xE000..=0xF8FF | 0xF0000..))
{
// Ligature characters of OpenType fonts, as their letters.
match c {
'\u{FB00}' => out.push_str("ff"),
'\u{FB01}' => out.push_str("fi"),
'\u{FB02}' => out.push_str("fl"),
'\u{FB03}' => out.push_str("ffi"),
'\u{FB04}' => out.push_str("ffl"),
_ => out.push(c),
}
}
}
}
/// Appends the text of the list from `p`.
fn list_text(&self, mut p: NodeRef, out: &mut String) {
while !p.is_null() {
match self.nodes.data(p) {
NodeData::Glyph(_) => self.glyph_text(p, out),
NodeData::Box(b) => self.list_text(b.head, out),
NodeData::Disc { replace, .. } => self.list_text(*replace, out),
NodeData::Glue { .. } if !out.ends_with(' ') => out.push(' '),
_ => {}
}
p = self.nodes.next(p);
}
}
}