retex-engine 1.0.0-rc.2

The TeX typesetting engine for retex
Documentation
//! 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);
        }
    }
}