Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 138 additions & 1 deletion crates/math-core/src/character_class.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<Self> {
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<Self> {
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<StretchableOp>,
close: Option<StretchableOp>,
style: Option<Style>,
) -> Node<'arena> {
fn to_operator(delim: Option<StretchableOp>) -> Node<'static> {
if let Some(op) = delim {
let attrs = if matches!(op.stretchy, Stretchy::Never) {
OpAttrs::STRETCHY_TRUE
} else {
OpAttrs::empty()
};
let (left, right) = if matches!(
op.spacing,
DelimiterSpacing::Relation | DelimiterSpacing::Other
) {
(Some(MathSpacing::Zero), Some(MathSpacing::Zero))
} else {
(None, None)
};
Node::Operator {
op: op.as_op(),
attrs,
size: None,
left,
right,
}
} else {
// An empty `<mo></mo>` produces weird spacing in some browsers.
// Use U+2063 (INVISIBLE SEPARATOR) to work around this. It's in Category K in MathML Core.
Node::Operator {
op: const { symbol::INVISIBLE_SEPARATOR.as_op() },
attrs: OpAttrs::empty(),
size: None,
left: None,
right: None,
}
}
}
let open = arena.push(to_operator(open));
let close = arena.push(to_operator(close));
content.insert(0, open);
content.push(close);
let nodes = arena.push_slice(&content);
Node::Row {
nodes,
attr: style.map(RowAttr::Style),
}
}

#[cfg(test)]
mod tests {
use super::{MathVariant, TextTransform};
Expand Down
47 changes: 22 additions & 25 deletions crates/math-core/src/environments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ use mathml_renderer::{
arena::Arena,
ast::Node,
attribute::Style,
symbol::{self, StretchableOp},
symbol,
table::{Alignment, ArraySpec},
};

use crate::character_class::{StretchableOp, fenced};

static ENVIRONMENTS: phf::Map<&'static str, Env> = phf::phf_map! {
"array" => Env::Array,
"subarray" => Env::Subarray,
Expand Down Expand Up @@ -165,13 +167,8 @@ impl Env {
style: None,
});
const OPEN_BRACE: StretchableOp =
symbol::LEFT_CURLY_BRACKET.as_stretchable_op().unwrap();
Node::Fenced {
open: Some(OPEN_BRACE),
close: None,
content,
style: None,
}
StretchableOp::from_ord(symbol::LEFT_CURLY_BRACKET).unwrap();
fenced(arena, vec![content], Some(OPEN_BRACE), None, None)
}
array_variant @ (Env::Array | Env::DArray | Env::Subarray) => {
// SAFETY: `array_spec` is guaranteed to be Some because we checked for
Expand Down Expand Up @@ -200,49 +197,49 @@ impl Env {
let (open, close) = match matrix_variant {
Env::PMatrix => {
const OPEN_PAREN: StretchableOp =
symbol::LEFT_PARENTHESIS.as_stretchable_op().unwrap();
StretchableOp::from_ord(symbol::LEFT_PARENTHESIS).unwrap();
const CLOSE_PAREN: StretchableOp =
symbol::RIGHT_PARENTHESIS.as_stretchable_op().unwrap();
StretchableOp::from_ord(symbol::RIGHT_PARENTHESIS).unwrap();
(OPEN_PAREN, CLOSE_PAREN)
}
Env::BMatrix => {
const OPEN_BRACKET: StretchableOp =
symbol::LEFT_SQUARE_BRACKET.as_stretchable_op().unwrap();
StretchableOp::from_ord(symbol::LEFT_SQUARE_BRACKET).unwrap();
const CLOSE_BRACKET: StretchableOp =
symbol::RIGHT_SQUARE_BRACKET.as_stretchable_op().unwrap();
StretchableOp::from_ord(symbol::RIGHT_SQUARE_BRACKET).unwrap();
(OPEN_BRACKET, CLOSE_BRACKET)
}
Env::Bmatrix => {
const OPEN_BRACE: StretchableOp =
symbol::LEFT_CURLY_BRACKET.as_stretchable_op().unwrap();
StretchableOp::from_ord(symbol::LEFT_CURLY_BRACKET).unwrap();
const CLOSE_BRACE: StretchableOp =
symbol::RIGHT_CURLY_BRACKET.as_stretchable_op().unwrap();
StretchableOp::from_ord(symbol::RIGHT_CURLY_BRACKET).unwrap();
(OPEN_BRACE, CLOSE_BRACE)
}
Env::VMatrix => {
const LINE: StretchableOp =
symbol::VERTICAL_LINE.as_stretchable_op().unwrap();
StretchableOp::from_ord(symbol::VERTICAL_LINE).unwrap();
(LINE, LINE)
}
Env::Vmatrix => {
const DOUBLE_LINE: StretchableOp =
symbol::DOUBLE_VERTICAL_LINE.as_stretchable_op().unwrap();
StretchableOp::from_ord(symbol::DOUBLE_VERTICAL_LINE).unwrap();
(DOUBLE_LINE, DOUBLE_LINE)
}
// SAFETY: `matrix_variant` is one of the strings above.
_ => unsafe { std::hint::unreachable_unchecked() },
_ => unreachable!(),
};
let style = Some(Style::Text);
Node::Fenced {
open: Some(open),
close: Some(close),
content: arena.push(Node::Table {
fenced(
arena,
vec![arena.push(Node::Table {
content,
align,
style,
}),
style: None,
}
})],
Some(open),
Some(close),
None,
)
}
}
}
Expand Down
58 changes: 28 additions & 30 deletions crates/math-core/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
ast::Node,
attribute::{LetterAttr, MathSpacing, OpAttrs, RowAttr, Style, TextTransform},
length::Length,
symbol::{
self, DelimiterSpacing, OpCategory, OrdCategory, RelCategory, StretchableOp, Stretchy,
},
symbol::{self, OpCategory, OrdCategory, RelCategory},
};
use rustc_hash::FxHashMap;

use crate::{
character_class::{Class, MathVariant, ParenType},
character_class::{
Class, DelimiterSpacing, MathVariant, ParenType, StretchableOp, Stretchy, fenced,
},
color_defs::get_color,
commands::get_negated_op,
environments::{Env, NumberedEnvState},
Expand Down Expand Up @@ -240,7 +240,7 @@
}
} else {
Err(LatexError(
span.into(),

Check warning on line 243 in crates/math-core/src/parser.rs

View workflow job for this annotation

GitHub Actions / clippy_check

useless conversion to the same type: `std::ops::Range<usize>`

warning: useless conversion to the same type: `std::ops::Range<usize>` --> crates/math-core/src/parser.rs:243:25 | 243 | span.into(), | ^^^^^^^^^^^ help: consider removing `.into()`: `span` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion = note: `#[warn(clippy::useless_conversion)]` on by default
LatexErrKind::CannotBeUsedHere {
got: LimitedUsabilityToken::Tag,
correct_place: Place::NumberedEnv,
Expand All @@ -252,14 +252,14 @@
let (label_name, _) = self.parse_string_literal()?;
if let Some(numbered_state) = &mut self.state.numbered {
if numbered_state.label.is_some() {
Err(LatexError(span.into(), LatexErrKind::MoreThanOneLabel))

Check warning on line 255 in crates/math-core/src/parser.rs

View workflow job for this annotation

GitHub Actions / clippy_check

useless conversion to the same type: `std::ops::Range<usize>`

warning: useless conversion to the same type: `std::ops::Range<usize>` --> crates/math-core/src/parser.rs:255:40 | 255 | Err(LatexError(span.into(), LatexErrKind::MoreThanOneLabel)) | ^^^^^^^^^^^ help: consider removing `.into()`: `span` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion
} else {
numbered_state.label = Some(label_name);
Ok(())
}
} else {
Err(LatexError(
span.into(),

Check warning on line 262 in crates/math-core/src/parser.rs

View workflow job for this annotation

GitHub Actions / clippy_check

useless conversion to the same type: `std::ops::Range<usize>`

warning: useless conversion to the same type: `std::ops::Range<usize>` --> crates/math-core/src/parser.rs:262:25 | 262 | span.into(), | ^^^^^^^^^^^ help: consider removing `.into()`: `span` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion
LatexErrKind::CannotBeUsedHere {
got: LimitedUsabilityToken::Label,
correct_place: Place::NumberedEnv,
Expand Down Expand Up @@ -645,21 +645,22 @@
if matches!(cur_token, Token::Binom(_)) {
let (lt_value, lt_unit) = Length::zero().into_parts();
const OPEN_PAREN: StretchableOp =
symbol::LEFT_PARENTHESIS.as_stretchable_op().unwrap();
StretchableOp::from_ord(symbol::LEFT_PARENTHESIS).unwrap();
const CLOSE_PAREN: StretchableOp =
symbol::RIGHT_PARENTHESIS.as_stretchable_op().unwrap();
Ok(Node::Fenced {
open: Some(OPEN_PAREN),
close: Some(CLOSE_PAREN),
content: self.commit(Node::Frac {
StretchableOp::from_ord(symbol::RIGHT_PARENTHESIS).unwrap();
Ok(fenced(
self.arena,
vec![self.commit(Node::Frac {
num,
denom,
lt_value,
lt_unit,
attr,
}),
style: None,
})
})],
Some(OPEN_PAREN),
Some(CLOSE_PAREN),
None,
))
} else {
let (lt_value, lt_unit) = Length::none().into_parts();
Ok(Node::Frac {
Expand Down Expand Up @@ -728,18 +729,19 @@
let denom = self.parse_next(ParseAs::Arg)?;
let attr = None;
let (lt_value, lt_unit) = lt.into_parts();
Ok(Node::Fenced {
open,
close,
content: self.commit(Node::Frac {
Ok(fenced(
self.arena,
vec![self.commit(Node::Frac {
num,
denom,
lt_value,
lt_unit,
attr,
}),
})],
open,
close,
style,
})
))
}
Token::Accent(op, is_over, attr) => {
let target = self.parse_next(ParseAs::ArgWithSpace)?;
Expand Down Expand Up @@ -1082,12 +1084,7 @@
} else {
Some(extract_delimiter(tok_loc, DelimiterModifier::Right)?)
};
Ok(Node::Fenced {
open: open_paren,
close: close_paren,
content: node_vec_to_node(self.arena, &content, false),
style: None,
})
Ok(fenced(self.arena, content, open_paren, close_paren, None))
}
Token::Middle => {
let tok_loc = self.next_token()?;
Expand Down Expand Up @@ -1941,12 +1938,13 @@

fn extract_delimiter(tok: TokSpan<'_>, location: DelimiterModifier) -> ParseResult<StretchableOp> {
let (tok, span) = tok.into_parts();
const SQ_L_BRACKET: StretchableOp = symbol::LEFT_SQUARE_BRACKET.as_stretchable_op().unwrap();
const SQ_R_BRACKET: StretchableOp = symbol::RIGHT_SQUARE_BRACKET.as_stretchable_op().unwrap();
const SQ_L_BRACKET: StretchableOp =
StretchableOp::from_ord(symbol::LEFT_SQUARE_BRACKET).unwrap();
const SQ_R_BRACKET: StretchableOp =
StretchableOp::from_ord(symbol::RIGHT_SQUARE_BRACKET).unwrap();
let delim = match tok {
Token::Open(paren) | Token::Close(paren) => paren.as_stretchable_op(),
Token::Ord(ord) => ord.as_stretchable_op(),
Token::Relation(rel) => rel.as_stretchable_op(),
Token::Ord(op) | Token::Open(op) | Token::Close(op) => StretchableOp::from_ord(op),
Token::Relation(rel) => StretchableOp::from_rel(rel),
Token::SquareBracketOpen => Some(SQ_L_BRACKET),
Token::SquareBracketClose => Some(SQ_R_BRACKET),
_ => None,
Expand Down
Loading
Loading