retex-engine 1.0.0-rc.2

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