use crate::node::{Node, NodeKind, StackKind};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Style {
Display,
Text,
Script,
ScriptScript,
}
impl Style {
#[inline(always)]
#[must_use]
pub const fn is_script(self) -> bool {
matches!(self, Self::Script | Self::ScriptScript)
}
#[inline(always)]
#[must_use]
pub const fn size_factor(self) -> f64 {
match self {
Self::Display | Self::Text => 1.0,
Self::Script => 0.7,
Self::ScriptScript => 0.5,
}
}
#[inline(always)]
#[must_use]
pub const fn sup(self) -> Self {
match self {
Self::Display | Self::Text => Self::Script,
Self::Script | Self::ScriptScript => Self::ScriptScript,
}
}
#[inline(always)]
#[must_use]
pub const fn num(self) -> Self {
match self {
Self::Display => Self::Text,
Self::Text => Self::Script,
Self::Script | Self::ScriptScript => Self::ScriptScript,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct StyleCtx {
pub style: Style,
pub cramped: bool,
}
impl StyleCtx {
#[inline(always)]
#[must_use]
pub const fn new(style: Style) -> Self {
Self {
style,
cramped: false,
}
}
#[inline(always)]
#[must_use]
pub const fn display() -> Self {
Self::new(Style::Display)
}
#[inline(always)]
#[must_use]
pub const fn sup(self) -> Self {
Self {
style: self.style.sup(),
cramped: self.cramped,
}
}
#[inline(always)]
#[must_use]
pub const fn sub(self) -> Self {
Self {
style: self.style.sup(),
cramped: true,
}
}
#[inline(always)]
#[must_use]
pub const fn num(self) -> Self {
Self {
style: self.style.num(),
cramped: self.cramped,
}
}
#[inline(always)]
#[must_use]
pub const fn den(self) -> Self {
Self {
style: self.style.num(),
cramped: true,
}
}
#[inline(always)]
#[must_use]
pub const fn cramp(self) -> Self {
Self {
style: self.style,
cramped: true,
}
}
#[inline(always)]
#[must_use]
pub const fn size_factor(self) -> f64 {
self.style.size_factor()
}
}
pub fn style_walk<'a, F>(node: &'a Node, ctx: StyleCtx, visit: &mut F)
where
F: FnMut(&'a Node, StyleCtx),
{
visit(node, ctx);
match &node.kind {
NodeKind::List(items) => walk_list(items, ctx, visit),
NodeKind::Scripts { base, sub, sup, .. } => {
if let Some(b) = base {
style_walk(b, ctx, visit);
}
if let Some(s) = sup {
style_walk(s, ctx.sup(), visit);
}
if let Some(s) = sub {
style_walk(s, ctx.sub(), visit);
}
}
NodeKind::Frac { num, den, spec } => {
let eff = match spec.forced_style {
Some(forced) => StyleCtx {
style: forced,
cramped: ctx.cramped,
},
None => ctx,
};
style_walk(num, eff.num(), visit);
style_walk(den, eff.den(), visit);
}
NodeKind::Radical { index, radicand } => {
if let Some(ix) = index {
style_walk(
ix,
StyleCtx {
style: Style::ScriptScript,
cramped: ctx.cramped,
},
visit,
);
}
style_walk(radicand, ctx.cramp(), visit);
}
NodeKind::Accent { base, .. } => style_walk(base, ctx.cramp(), visit),
NodeKind::LeftRight { body, .. } => walk_list(body, ctx, visit),
NodeKind::Text { body } | NodeKind::TextStyled { body, .. } => {
walk_list(body, ctx, visit);
}
NodeKind::MathIsland { body, display } => {
let style = if *display {
Style::Display
} else {
Style::Text
};
walk_list(body, StyleCtx::new(style), visit);
}
NodeKind::MathFont { body, .. } | NodeKind::Phantom { body, .. } => {
style_walk(body, ctx, visit);
}
NodeKind::Stack {
kind,
annotation,
base,
} => {
let ann_ctx = match kind {
StackKind::Underset => ctx.sub(),
StackKind::Stackrel | StackKind::Overset => ctx.sup(),
};
style_walk(annotation, ann_ctx, visit);
style_walk(base, ctx, visit);
}
NodeKind::XArrow { above, below, .. } => {
style_walk(above, ctx.sup(), visit);
if let Some(below) = below {
style_walk(below, ctx.sub(), visit);
}
}
NodeKind::AlignBlock { lines, .. } => {
for line in lines {
style_walk(line, ctx, visit);
}
}
NodeKind::Environment { name, rows, .. } => {
let cell_ctx = if name == "substack" {
StyleCtx::new(Style::Script)
} else if name.starts_with("align") {
ctx
} else {
StyleCtx::new(Style::Text)
};
for row in rows {
for cell in row {
style_walk(cell, cell_ctx, visit);
}
}
}
NodeKind::Symbol { .. }
| NodeKind::BigOp { .. }
| NodeKind::OpName { .. }
| NodeKind::SizedDelim { .. }
| NodeKind::TextRun { .. }
| NodeKind::StyleChange(_)
| NodeKind::SizeChange(_)
| NodeKind::AlignChange(_)
| NodeKind::LineSpacing(_)
| NodeKind::ColorChange(_)
| NodeKind::Space(_)
| NodeKind::Tie
| NodeKind::Linebreak
| NodeKind::AlignTab
| NodeKind::Fragment(_) => {}
}
}
fn walk_list<'a, F>(items: &'a [Node], mut ctx: StyleCtx, visit: &mut F)
where
F: FnMut(&'a Node, StyleCtx),
{
for item in items {
if let NodeKind::StyleChange(style) = &item.kind {
visit(item, ctx);
ctx = StyleCtx {
style: *style,
cramped: ctx.cramped,
};
continue;
}
style_walk(item, ctx, visit);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sup_chain() {
assert_eq!(Style::Display.sup(), Style::Script);
assert_eq!(Style::Text.sup(), Style::Script);
assert_eq!(Style::Script.sup(), Style::ScriptScript);
assert_eq!(Style::ScriptScript.sup(), Style::ScriptScript);
}
#[test]
fn num_chain() {
assert_eq!(Style::Display.num(), Style::Text);
assert_eq!(Style::Text.num(), Style::Script);
assert_eq!(Style::Script.num(), Style::ScriptScript);
assert_eq!(Style::ScriptScript.num(), Style::ScriptScript);
}
#[test]
fn sub_is_cramped_sup() {
let ctx = StyleCtx::display();
assert_eq!(ctx.sub().style, Style::Script);
assert!(ctx.sub().cramped);
assert!(!ctx.sup().cramped);
}
#[test]
fn den_is_cramped_num() {
let ctx = StyleCtx::new(Style::Text);
assert_eq!(ctx.den().style, Style::Script);
assert!(ctx.den().cramped);
}
#[test]
fn size_factors_are_cm_10_7_5() {
assert_eq!(Style::Display.size_factor(), 1.0);
assert_eq!(Style::Text.size_factor(), 1.0);
assert_eq!(Style::Script.size_factor(), 0.7);
assert_eq!(Style::ScriptScript.size_factor(), 0.5);
}
}