1use std::{borrow::Cow, fmt, rc::Rc};
2
3use libxml::tree::Node;
4
5use crate::{
6 BoxOps, Digested,
7 common::{
8 arena::{self, SymHashMap as HashMap, SymStr},
9 dimension::Dimension,
10 error::*,
11 font::Font,
12 locator::Locator,
13 object::Object,
14 store::Stored,
15 },
16 document::Document,
17 gullet, pin,
18 state::{lookup_font, with_value},
19 token::{Catcode, Token},
20 tokens::Tokens,
21};
22
23#[derive(Debug, Clone)]
25pub struct Tbox {
26 pub text: SymStr,
28 pub font: Rc<Font>,
30 pub locator: Option<Locator>,
32 pub properties: HashMap<Stored>,
34 pub tokens: Tokens,
36}
37
38impl Default for Tbox {
39 fn default() -> Self {
40 Tbox {
41 text: pin!(""),
42 font: Rc::new(Font::text_default()),
43 locator: None,
44 properties: HashMap::default(),
45 tokens: Tokens!(),
46 }
47 }
48}
49
50impl PartialEq for Tbox {
51 fn eq(&self, other: &Self) -> bool { self.text == other.text && *self.font == *other.font }
53}
54
55impl fmt::Display for Tbox {
58 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
59 arena::with(self.text, |text| write!(f, "{}", text))
60 }
61}
62impl Object for Tbox {
63 fn get_locator(&self) -> Option<Locator> { self.locator }
64 fn revert(&self) -> Result<Tokens> { Ok(self.tokens.clone()) }
65 fn stringify(&self) -> String { format!("{self:?}") }
66}
67impl Tbox {
68 pub fn new(
74 text: SymStr,
75 font_opt: Option<Rc<Font>>,
76 locator_opt: Option<Locator>,
77 tokens_opt: Tokens,
78 mut properties: HashMap<Stored>,
79 ) -> Self {
80 let locator = Some(locator_opt.unwrap_or_else(gullet::get_locator));
83 let mut font = match font_opt {
84 Some(f) => f,
85 None => lookup_font().unwrap(),
86 };
87 let empty_sym = pin!("");
88 let tokens = if text != empty_sym && tokens_opt.is_empty() {
89 Tokens!(Token {
90 text,
91 code: Catcode::OTHER,
92 #[cfg(feature = "token-locators")]
93 loc: 0
94 })
95 } else {
96 tokens_opt
97 };
98
99 if !properties.contains_key("isSpace") && text != empty_sym {
103 let is_all_ws = arena::with(text, |s| {
104 !s.is_empty() && s.chars().all(|c| c.is_whitespace())
105 });
106 if is_all_ws {
107 properties.insert("isSpace", Stored::Bool(true));
108 }
109 }
110
111 if properties.contains_key("isSpace")
112 && (properties.contains_key("width")
113 || properties.contains_key("height")
114 || properties.contains_key("depth"))
115 {
116 properties
117 .entry("width")
118 .or_insert_with(|| Stored::Dimension(Dimension::default()));
119 properties
120 .entry("height")
121 .or_insert_with(|| Stored::Dimension(Dimension::default()));
122 properties
123 .entry("depth")
124 .or_insert_with(|| Stored::Dimension(Dimension::default()));
125 }
126 if crate::state::lookup_bool_sym(crate::pin!("IN_MATH")) {
127 properties.insert("mode", Stored::String(pin!("math")));
128 if text != empty_sym {
129 with_value(
130 &arena::with(text, |text_str| s!("math_token_attributes_{}", text_str)),
131 |value_opt| {
132 if let Some(Stored::HashString(attr)) = value_opt {
133 for (key, value) in attr.iter() {
134 properties
135 .entry(key)
136 .or_insert_with(|| Stored::String(arena::pin(value)));
137 }
138 }
139 },
140 );
141 }
142 font = Rc::new(arena::with(text, |text_str| font.specialize(text_str)));
143 }
144 Tbox {
145 text,
146 font,
147 locator,
148 properties,
149 tokens,
150 }
151 }
152 pub fn is_empty(&self) -> bool {
154 self.get_property_bool("isEmpty")
157 || self.get_property_bool("isSpace")
158 || arena::with(self.text, |text| text.trim().is_empty())
159 }
160
161 pub fn is_math(&self) -> bool {
165 match self.properties.get("mode") {
166 Some(Stored::String(s)) => arena::with(*s, |m| m.ends_with("math")),
167 _ => false,
168 }
169 }
170
171 pub fn set_properties<I>(&mut self, entries: I)
175 where I: IntoIterator<Item = (&'static str, Stored)> {
176 for (key, value) in entries {
177 self.properties.insert(key, value);
178 }
179 }
180
181 pub fn total_height(&self) -> Dimension {
185 let h = match self.properties.get("height") {
186 Some(Stored::Dimension(d)) => d.0,
187 _ => 0,
188 };
189 let d = match self.properties.get("depth") {
190 Some(Stored::Dimension(d)) => d.0,
191 _ => 0,
192 };
193 Dimension(h + d)
194 }
195}
196
197impl BoxOps for Tbox {
198 fn get_tokens(&self) -> Option<&Tokens> { Some(&self.tokens) }
199 fn get_properties(&self) -> &HashMap<Stored> { &self.properties }
200 fn get_property(&self, key: &str) -> Option<Cow<'_, Stored>> {
201 let props = &self.properties;
202 if key == "isSpace" {
203 match props.get(key) {
204 Some(value) => Some(Cow::Owned(value.clone())),
205 None => {
206 let tex = self
207 .get_tokens()
208 .map(|tks| tks.clone().untex())
209 .unwrap_or_default(); if !tex.is_empty() && tex.chars().all(char::is_whitespace) {
211 Some(Cow::Owned(Stored::Bool(true)))
213 } else {
214 None
215 }
216 },
217 }
218 } else {
219 props.get(key).map(|v| Cow::Owned(v.clone()))
220 }
221 }
222 fn with_properties<R, FnR>(&self, caller: FnR) -> R
223 where FnR: FnOnce(&HashMap<Stored>) -> R {
224 caller(&self.properties)
225 }
226 fn get_properties_mut(&mut self) -> &mut HashMap<Stored> { &mut self.properties }
227 fn get_string(&self) -> Result<Cow<'_, str>> {
228 Ok(Cow::Owned(arena::with(self.text, |text| text.to_string())))
230 }
231
232 fn be_absorbed(&self, document: &mut Document) -> Result<Vec<Node>> {
233 let text = self.get_string()?;
234 let font = &self.font;
235 let mode = match self.properties.get("mode") {
236 Some(Stored::String(s)) => *s,
237 _ => pin!("text"),
238 };
239
240 if !text.is_empty() {
241 let mode_is_math = arena::with(mode, |m| m.ends_with("math"));
243 if mode_is_math {
244 let in_math_context = {
249 let mut node = document.node.clone();
250 let mut found = false;
251 loop {
252 let qname = crate::document::get_node_qname(&node);
253 let hit = arena::with(qname, |s| s.contains("XM") || s.contains("Math"));
254 if hit {
255 found = true;
256 break;
257 }
258 match node.get_parent() {
259 Some(parent) => node = parent,
260 None => break,
261 }
262 }
263 found
264 };
265 if in_math_context {
266 Ok(vec![document.insert_math_token(
267 &text,
268 Stored::cast_to_string_hash(&self.properties),
269 Some(font),
270 )?])
271 } else {
272 match document.open_text(&text, font)? {
274 None => Ok(Vec::new()),
275 Some(node) => Ok(vec![node]),
276 }
277 }
278 } else {
279 match document.open_text(&text, font)? {
280 None => Ok(Vec::new()),
281 Some(node) => Ok(vec![node]),
282 }
283 }
284 } else {
285 Ok(Vec::new())
286 }
287 }
288
289 fn get_font(&self) -> Result<Option<Rc<Font>>> { Ok(Some(Rc::clone(&self.font))) }
290
291 fn compute_size(&self, options: HashMap<Stored>) -> Result<(Dimension, Dimension, Dimension)> {
292 match self.get_property("body") {
293 Some(body_stored) => {
294 if let Stored::Digested(ref body) = *body_stored {
295 body.compute_size(options)
296 } else {
297 panic!("the stored 'body' property should always be a Stored::Digested enum case.");
298 }
299 },
300 _ => Ok(self.font.compute_string_size(&self.get_string()?, options)),
301 }
302 }
303}
304
305impl From<Tbox> for Result<Vec<Digested>> {
306 fn from(tbox: Tbox) -> Result<Vec<Digested>> { Ok(vec![Digested::from(tbox)]) }
307}
308impl From<Tbox> for Option<Digested> {
309 fn from(tbox: Tbox) -> Option<Digested> { Some(Digested::from(tbox)) }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn tbox_default_has_empty_text() {
318 let t = Tbox::default();
319 assert_eq!(arena::to_string(t.text), "");
320 assert_eq!(t.properties.len(), 0);
321 assert_eq!(t.tokens.len(), 0);
322 }
323
324 #[test]
325 fn tbox_display_of_default_is_empty() {
326 let t = Tbox::default();
327 assert_eq!(format!("{t}"), "");
328 }
329
330 #[test]
331 fn tbox_display_of_text_content() {
332 let t = Tbox {
333 text: arena::pin("hello"),
334 ..Default::default()
335 };
336 assert_eq!(format!("{t}"), "hello");
337 }
338
339 #[test]
340 fn tbox_partial_eq_same_text_same_font() {
341 let a = Tbox::default();
342 let b = Tbox::default();
343 assert_eq!(
344 a, b,
345 "two default Tboxes have same text '' and same text_default font"
346 );
347 }
348
349 #[test]
350 fn tbox_partial_eq_different_text() {
351 let a = Tbox::default();
352 let b = Tbox {
353 text: arena::pin("X"),
354 ..Default::default()
355 };
356 assert_ne!(a, b);
357 }
358
359 #[test]
360 fn tbox_default_font_is_text_default() {
361 let t = Tbox::default();
362 assert_eq!(*t.font, Font::text_default());
364 }
365
366 #[test]
367 fn tbox_default_locator_is_default() {
368 let t = Tbox::default();
369 let _ = t.locator;
372 }
373}