diff --git a/crates/math-core/src/character_class.rs b/crates/math-core/src/character_class.rs index fe83be80..5688426b 100644 --- a/crates/math-core/src/character_class.rs +++ b/crates/math-core/src/character_class.rs @@ -1,4 +1,9 @@ -use mathml_renderer::attribute::TextTransform; +use mathml_renderer::{ + arena::Arena, + ast::Node, + attribute::{MathSpacing, OpAttrs, RowAttr, Style, TextTransform}, + symbol::{self, MathMLOperator, OrdCategory, OrdLike, Rel, RelCategory}, +}; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum Class { @@ -39,6 +44,138 @@ pub enum MathVariant { Transform(TextTransform), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Stretchy { + /// The operator is always stretchy (e.g. `(`, `)`). + Always = 1, + /// The operator is only stretchy as a pre- or postfix operator (e.g. `|`). + PrePostfix, + /// The operator is never stretchy (e.g. `/`). + Never, + /// The operator is always stretchy but isn't symmetric (e.g. `↑`). + AlwaysAsymmetric, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DelimiterSpacing { + /// Never has any spacing, even when used as an infix operator (e.g. `(`, `)`). + Zero, + /// Has relation spacing when used as an infix operator, but not when used as a prefix or + /// postfix operator (e.g. `|`). + InfixRelation, + /// Always has relation spacing, even when used as a prefix or postfix operator (e.g. `↑`). + Relation, + /// Always has some spacing, even when used as a prefix or postfix operator (e.g. `/`). + Other, +} + +/// A stretchable operator. +/// +/// It can be created from an `OrdLike` or a `Rel` if the operator is stretchable. This struct +/// carries all the information needed to know how to make the operator stretchy and how to set +/// spacing around it. +#[derive(Debug, Clone, PartialEq, Eq, Copy)] +pub struct StretchableOp { + op: MathMLOperator, + pub stretchy: Stretchy, + pub spacing: DelimiterSpacing, +} + +impl StretchableOp { + #[inline] + pub const fn as_op(self) -> MathMLOperator { + self.op + } + + /// Creates a `StretchableOp` from an `OrdLike` if it's stretchable. Returns `None` if the + /// operator isn't stretchable. + pub const fn from_ord(ord: OrdLike) -> Option { + let (stretchy, spacing) = match ord.category() { + OrdCategory::F | OrdCategory::G => (Stretchy::Always, DelimiterSpacing::Zero), + OrdCategory::FGandForceDefault => { + (Stretchy::PrePostfix, DelimiterSpacing::InfixRelation) + } + OrdCategory::K => (Stretchy::Never, DelimiterSpacing::Zero), + OrdCategory::KButUsedToBeB => (Stretchy::Never, DelimiterSpacing::Other), + OrdCategory::D | OrdCategory::E | OrdCategory::I | OrdCategory::IK => { + return None; + } + }; + Some(StretchableOp { + op: ord.as_op(), + stretchy, + spacing, + }) + } + + /// Creates a `StretchableOp` from a `Rel` if it's stretchable. Returns `None` if the operator + /// isn't stretchable. + pub const fn from_rel(rel: Rel) -> Option { + match rel.category() { + RelCategory::A => Some(StretchableOp { + op: rel.as_op(), + stretchy: Stretchy::AlwaysAsymmetric, + spacing: DelimiterSpacing::Relation, + }), + RelCategory::Default => None, + } + } +} + +/// Creates a fenced expression where opening and closing delimiters are stretched to fit the height +/// of the content. If `open` or `close` is `None`, no delimiter will be rendered on that side. +pub fn fenced<'arena>( + arena: &'arena Arena, + mut content: Vec<&'arena Node<'arena>>, + open: Option, + close: Option, + style: Option