use crate::layout_box::{BoxContent, LayoutBox, VBoxChildKind};
pub const MAX_LAYOUT_OUTPUT_ITEMS: usize = 65_536;
pub(crate) fn vertical_dash_count(height: f64, thickness: f64) -> usize {
(height / (8.0 * thickness)).ceil().max(0.0) as usize
}
fn own_cost(box_: &LayoutBox) -> Result<usize, String> {
Ok(match &box_.content {
BoxContent::Glyph { .. } | BoxContent::Rule { .. } | BoxContent::SvgPath { .. } => 1,
BoxContent::GlyphRun { glyphs } => glyphs.len(),
BoxContent::Fraction { bar_thickness, .. } => usize::from(*bar_thickness > 0.0),
BoxContent::Radical { .. } => 2, BoxContent::Angl { .. } | BoxContent::Overline { .. } | BoxContent::Underline { .. } => 1,
BoxContent::Framed {
has_border,
bg_color,
..
} => 4 * usize::from(*has_border) + usize::from(bg_color.is_some()),
BoxContent::ProofTree { rules, .. } => rules.len(),
BoxContent::Array {
col_separators,
hlines_before_row,
rule_thickness,
..
} => {
let mut cost = hlines_before_row
.iter()
.fold(0usize, |sum, row| sum.saturating_add(row.len()));
let dashed = col_separators
.iter()
.filter(|sep| **sep == Some(true))
.count();
let solid = col_separators
.iter()
.filter(|sep| **sep == Some(false))
.count();
cost = cost.saturating_add(solid);
if dashed > 0 {
let height = box_.height + box_.depth;
let period = 8.0 * rule_thickness;
if !height.is_finite() || height < 0.0 || !period.is_finite() || period <= 0.0 {
return Err("Invalid dashed separator geometry".into());
}
cost = cost.saturating_add(
vertical_dash_count(height, *rule_thickness).saturating_mul(dashed),
);
}
cost
}
BoxContent::HBox(_)
| BoxContent::VBox(_)
| BoxContent::SupSub { .. }
| BoxContent::OpLimits { .. }
| BoxContent::Accent { .. }
| BoxContent::LeftRight { .. }
| BoxContent::RaiseBox { .. }
| BoxContent::Scaled { .. }
| BoxContent::Kern
| BoxContent::Empty => 0,
})
}
fn children<'a>(box_: &'a LayoutBox, mut visit: impl FnMut(&'a LayoutBox)) {
match &box_.content {
BoxContent::HBox(boxes) => boxes.iter().for_each(visit),
BoxContent::VBox(boxes) => {
for child in boxes {
if let VBoxChildKind::Box(body) = &child.kind {
visit(body);
}
}
}
BoxContent::Fraction { numer, denom, .. } => {
visit(numer);
visit(denom);
}
BoxContent::SupSub { base, sup, sub, .. } | BoxContent::OpLimits { base, sup, sub, .. } => {
visit(base);
if let Some(sup) = sup {
visit(sup);
}
if let Some(sub) = sub {
visit(sub);
}
}
BoxContent::Radical { body, index, .. } => {
visit(body);
if let Some(index) = index {
visit(index);
}
}
BoxContent::Accent { base, accent, .. } => {
visit(base);
visit(accent);
}
BoxContent::LeftRight { left, right, inner } => {
visit(left);
visit(right);
visit(inner);
}
BoxContent::Array {
cells,
row_tags,
tag_col_width,
..
} => {
for row in cells {
for cell in row {
visit(cell);
}
}
if *tag_col_width > 0.0 {
for tag in row_tags.iter().flatten() {
visit(tag);
}
}
}
BoxContent::Framed { body, .. }
| BoxContent::RaiseBox { body, .. }
| BoxContent::Scaled { body, .. }
| BoxContent::Angl { body, .. }
| BoxContent::Overline { body, .. }
| BoxContent::Underline { body, .. } => visit(body),
BoxContent::ProofTree { children, .. } => {
for child in children {
visit(&child.box_);
}
}
BoxContent::Glyph { .. }
| BoxContent::GlyphRun { .. }
| BoxContent::Rule { .. }
| BoxContent::SvgPath { .. }
| BoxContent::Kern
| BoxContent::Empty => {}
}
}
pub fn validate_layout(root: &LayoutBox) -> Result<(), String> {
output_cost(root).map(|_| ())
}
fn output_cost(root: &LayoutBox) -> Result<usize, String> {
let mut pending = vec![root];
let mut cost = 0usize;
while let Some(box_) = pending.pop() {
cost = cost.saturating_add(own_cost(box_)?);
if cost > MAX_LAYOUT_OUTPUT_ITEMS {
return Err("Layout resource limit exceeded".into());
}
children(box_, |child| pending.push(child));
}
Ok(cost)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{layout, to_display_list, LayoutOptions};
use ratex_types::math_style::MathStyle;
#[test]
fn layout_cost_matches_output_across_primitives_and_scales() {
let formulas = [
r"x^2_i+\frac{a}{b}+\binom{n}{k}",
r"\sqrt[3]{x}+\sum_{i=0}^n i+\hat{x}+\overline{x}+\underline{y}",
r"\text{abc}+\colorbox{red}{x}+\fbox{x}+\angl{x}",
r"\left(\frac{x}{y}\middle|z\right)",
r"\pmb{\pmb{\begin{array}{:c|r:}\hline x&y\\z\end{array}}}",
r"\begin{align}x&=y\tag{one}\\z&=0\tag{two}\end{align}",
r"\begin{prooftree}\AxiomC{P}\UnaryInfC{Q}\end{prooftree}",
r"\tiny\begin{array}{:c:}\rule{1em}{10em}\end{array}",
r"\htmlStyle{font-size:2em;}{\begin{array}{:c:}x\end{array}}",
r"\raisebox{10000000000000000em}{\begin{array}{:c:}x\end{array}}",
r"\phantom{x}+\vphantom{y}+\smash{z}+\mathllap{w}",
];
for style in [
MathStyle::Display,
MathStyle::Text,
MathStyle::Script,
MathStyle::ScriptScript,
] {
for input in formulas {
let ast =
ratex_parser::parse(input).unwrap_or_else(|err| panic!("{input}: {err:?}"));
let boxes = layout(&ast, &LayoutOptions::default().with_style(style));
assert_eq!(
output_cost(&boxes).unwrap(),
to_display_list(&boxes).items.len(),
"{input} ({style:?})"
);
}
}
}
#[test]
fn invalid_dashed_dimensions_are_rejected_without_conversion() {
let ast = ratex_parser::parse(r"\begin{array}{:c}x\end{array}").unwrap();
let BoxContent::HBox(mut boxes) = layout(&ast, &LayoutOptions::default()).content else {
panic!("expected expression");
};
let mut array = boxes.remove(0);
assert!(matches!(array.content, BoxContent::Array { .. }));
for height in [f64::NAN, f64::INFINITY, -1.0] {
array.height = height;
array.depth = 0.0;
assert_eq!(
output_cost(&array).unwrap_err(),
"Invalid dashed separator geometry"
);
}
array.height = 1.0;
for thickness in [0.0, -1.0, f64::NAN, f64::INFINITY, f64::MAX] {
if let BoxContent::Array { rule_thickness, .. } = &mut array.content {
*rule_thickness = thickness;
}
assert_eq!(
output_cost(&array).unwrap_err(),
"Invalid dashed separator geometry"
);
}
}
#[test]
fn dash_count_handles_full_period_and_partial_tail() {
assert_eq!(vertical_dash_count(0.0, 0.0625), 0);
assert_eq!(vertical_dash_count(2.0, 0.0625), 4);
assert_eq!(vertical_dash_count(2.01, 0.0625), 5);
}
}