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
12 changes: 11 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 37 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ The documentation for the library can be found on docs.rs: https://docs.rs/math-
### WebAssembly module
The WebAssembly frontend is currently only used for the [playground](https://tmke8.github.io/math-core/). In the future, a package for this may be published to npm.

## Specifying the math font
The MathML code generated by this project is intended to be very portable and work without a CSS style sheet. However, in order to really get LaTeX-like rendering of the MathML, one unfortunately needs custom math fonts.
## Required CSS
### Specifying the math font
The MathML code generated by this project is intended to be very portable and work *almost* without a CSS style sheet. However, in order to really get LaTeX-like rendering of the MathML, one unfortunately needs custom math fonts.

To specify the font, include something like this in your CSS:

Expand All @@ -113,9 +114,42 @@ The main problem is that Chromium does not look at `ssty` variants when deciding

The fixes applied to the font files do not change the shape of any glyphs; they merely rearrange some glyphs or center them. The font files can be found in the `playground/` directory in this repository. To use them in your website, download them here and load them on the page from your server. No guarantees will be made that the fonts on the playground will remain available on the current URLs.

### Font subsetting
#### Font subsetting
The math fonts all have quite large font files. Especially *New Computer Modern Math Book* is enormous with an almost 700kB `.woff2` file. Therefore, if possible, you should use *font subsetting* where the font file only includes those glyphs that are actually used on your website. Existing tools should work fine for this.

### CSS for polyfills
Chromium does not support the `<menclose>` element, which is used for `\sout{...}`, `\cancel{...}`, etc. Therefore, if you want to use these commands, you need to include the following CSS:

```css
/* Styles for Chromium only */
@supports (not (-webkit-backdrop-filter: blur(1px))) and (not (-moz-appearance: none)) {
menclose {
position: relative;
padding: 0.5ex 0;
}
mrow.menclose-updiagonalstrike,
mrow.menclose-downdiagonalstrike,
mrow.menclose-horizontalstrike {
display: inline-block;
position: absolute;
left: 0.5px;
bottom: 0;
width: 100%;
height: 100%;
background-color: currentcolor;
}
mrow.menclose-updiagonalstrike {
clip-path: polygon(0.05em 100%, 0 calc(100% - 0.05em), calc(100% - 0.05em) 0, 100% 0.05em);
}
mrow.menclose-downdiagonalstrike {
clip-path: polygon(0em 0.05em, 0.05em 0em, 100% calc(100% - 0.05em), calc(100% - 0.05em) 100%);
}
mrow.menclose-horizontalstrike {
clip-path: polygon(0em calc(55% + 0.0333em), 0em calc(55% - 0.0333em), 100% calc(55% - 0.0333em), 100% calc(55% + 0.0333em));
}
}
```

## Development status
There are two tracking issues:

Expand Down
2 changes: 2 additions & 0 deletions math-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ phf = { version = "0.12.1", features = ["macros"] }
strum = "0.27.1"
strum_macros = "0.27.2"
rustc-hash = { workspace = true }
bitflags = "2.9.3"

[dev-dependencies]
insta = { version = "1.41.1", features = ["default", "ron"] }
regex = "1.11.1"
math-core = { path = ".", features = ["serde"] }
minijinja = "2.11.0"
bitflags = { version = "2.9.3", features = ["serde"] }

[features]
serde = ["dep:serde"]
6 changes: 5 additions & 1 deletion math-core/src/latex_parser/commands.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::mathml_renderer::ast::Node;
use crate::mathml_renderer::attribute::{
FracAttr, MathSpacing, MathVariant, OpAttr, RowAttr, Size, Style, TextTransform,
FracAttr, MathSpacing, MathVariant, Notation, OpAttr, RowAttr, Size, Style, TextTransform,
};
use crate::mathml_renderer::symbol::{self, Rel};

Expand Down Expand Up @@ -137,6 +137,7 @@ static COMMANDS: phf::Map<&'static str, Token> = phf::phf_map! {
"backtrprime" => Token::Ord(symbol::REVERSED_TRIPLE_PRIME),
"bar" => Token::OverUnder(symbol::MACRON, true, Some(OpAttr::StretchyFalse)),
"barwedge" => Token::BinaryOp(symbol::NAND),
"bcancel" => Token::Enclose(Notation::DOWN_DIAGONAL),
"because" => Token::Relation(symbol::BECAUSE),
"begin" => Token::Begin,
"beta" => Token::Letter(symbol::GREEK_SMALL_LETTER_BETA),
Expand Down Expand Up @@ -195,6 +196,7 @@ static COMMANDS: phf::Map<&'static str, Token> = phf::phf_map! {
"breve" => Token::OverUnder(symbol::BREVE, true, None),
"bullet" => Token::BinaryOp(symbol::BULLET_OPERATOR),
"bumpeq" => Token::Relation(symbol::DIFFERENCE_BETWEEN),
"cancel" => Token::Enclose(Notation::UP_DIAGONAL),
"cap" => Token::BinaryOp(symbol::INTERSECTION),
"cdot" => Token::BinaryOp(symbol::MIDDLE_DOT),
"cdots" => Token::CustomCmd(0, NodeRef::new(&Node::Row {
Expand Down Expand Up @@ -616,6 +618,7 @@ static COMMANDS: phf::Map<&'static str, Token> = phf::phf_map! {
"simeq" => Token::Relation(symbol::ASYMPTOTICALLY_EQUAL_TO),
"slashed" => Token::Slashed,
"smile" => Token::Relation(symbol::SMILE),
"sout" => Token::Enclose(Notation::HORIZONTAL),
"spadesuit" => Token::Letter(symbol::BLACK_SPADE_SUIT),
"sphericalangle" => Token::Letter(symbol::SPHERICAL_ANGLE),
"sqcap" => Token::BinaryOp(symbol::SQUARE_CAP),
Expand Down Expand Up @@ -727,6 +730,7 @@ static COMMANDS: phf::Map<&'static str, Token> = phf::phf_map! {
"widetilde" => Token::OverUnder(symbol::TILDE, true, None),
"wp" => Token::Letter(symbol::SCRIPT_CAPITAL_P),
"wr" => Token::Relation(symbol::WREATH_PRODUCT),
"xcancel" => Token::Enclose(Notation::UP_DIAGONAL.union(Notation::DOWN_DIAGONAL)),
"xi" => Token::Letter(symbol::GREEK_SMALL_LETTER_XI),
"xleftarrow" => Token::CustomCmd(1, NodeRef::new(&predefined::XLEFTARROW)),
"xrightarrow" => Token::CustomCmd(1, NodeRef::new(&predefined::XRIGHTARROW)),
Expand Down
5 changes: 5 additions & 0 deletions math-core/src/latex_parser/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,10 @@ where
};
return self.make_pseudo_operator(name, &prev_state);
}
Token::Enclose(notation) => {
let content = self.parse_next(ParseAs::ArgWithSpace)?;
Node::Enclose { content, notation }
}
Token::Space(space) => {
// Spaces pass through the sequence state.
if let Some(state) = sequence_state {
Expand Down Expand Up @@ -1584,6 +1588,7 @@ mod tests {
("overset_digits", r"\overset12"),
("genfrac", r"\genfrac(){1pt}{0}{1}{2}"),
("mspace", r"\mspace{1mu}"),
("sout", r"\sout{abc}"),
];
for (name, problem) in problems.into_iter() {
let arena = Arena::new();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
source: math-core/src/latex_parser/parse.rs
expression: "\\sout{abc}"
---
[
Enclose(
content: Row(
nodes: [
IdentifierChar('a', Default),
IdentifierChar('b', Default),
IdentifierChar('c', Default),
],
attr: None,
),
notation: Notation("HORIZONTAL"),
),
]
3 changes: 2 additions & 1 deletion math-core/src/latex_parser/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use strum_macros::IntoStaticStr;

use crate::mathml_renderer::ast::Node;
use crate::mathml_renderer::attribute::{
FracAttr, MathVariant, OpAttr, Size, Style, TextTransform,
FracAttr, MathVariant, Notation, OpAttr, Size, Style, TextTransform,
};
use crate::mathml_renderer::length::Length;
use crate::mathml_renderer::symbol::{Bin, Fence, Op, Ord, Punct, Rel};
Expand Down Expand Up @@ -92,6 +92,7 @@ pub enum Token<'source> {
Number(Digit),
// For `\log`, `\exp`, `\sin`, `\cos`, `\tan`, etc.
PseudoOperator(&'source str),
Enclose(Notation),
#[strum(serialize = r"\operatorname")]
OperatorName,
Slashed,
Expand Down
72 changes: 72 additions & 0 deletions math-core/src/mathml_renderer/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use std::fmt::Write;
#[cfg(feature = "serde")]
use serde::Serialize;

use crate::mathml_renderer::attribute::Notation;

use super::attribute::{
FracAttr, LetterAttr, MathSpacing, MathVariant, OpAttr, RowAttr, Size, StretchMode, Stretchy,
Style, TextTransform,
Expand Down Expand Up @@ -121,6 +123,11 @@ pub enum Node<'arena> {
ColumnSeparator,
/// `<mtr>...</mtr>`
RowSeparator,
/// <menclose>...</menclose>
Enclose {
content: &'arena Node<'arena>,
notation: Notation,
},
Slashed(&'arena Node<'arena>),
Multiscript {
base: &'arena Node<'arena>,
Expand Down Expand Up @@ -530,6 +537,51 @@ impl<'converter, 'arena> MathMLEmitter<'converter, 'arena> {
self.emit_table(base_indent, child_indent, content, mtd_opening, false)?;
}
Node::ColumnSeparator | Node::RowSeparator => (),
Node::Enclose { content, notation } => {
let notation = *notation;
write!(self.s, "<menclose notation=\"")?;
let mut first = true;
if notation.contains(Notation::UP_DIAGONAL) {
write!(self.s, "updiagonalstrike")?;
first = false;
}
if notation.contains(Notation::DOWN_DIAGONAL) {
if !first {
write!(self.s, " ")?;
}
write!(self.s, "downdiagonalstrike")?;
}
if notation.contains(Notation::HORIZONTAL) {
if !first {
write!(self.s, " ")?;
}
write!(self.s, "horizontalstrike")?;
}
write!(self.s, "\">")?;
self.emit(content, child_indent)?;
if notation.contains(Notation::UP_DIAGONAL) {
writeln_indent!(
&mut self.s,
child_indent,
"<mrow class=\"menclose-updiagonalstrike\"></mrow>"
);
}
if notation.contains(Notation::DOWN_DIAGONAL) {
writeln_indent!(
&mut self.s,
child_indent,
"<mrow class=\"menclose-downdiagonalstrike\"></mrow>"
);
}
if notation.contains(Notation::HORIZONTAL) {
writeln_indent!(
&mut self.s,
child_indent,
"<mrow class=\"menclose-horizontalstrike\"></mrow>"
);
}
writeln_indent!(&mut self.s, base_indent, "</menclose>");
}
Node::CustomCmd { predefined, args } => {
let old_args = self.custom_cmd_args.replace(args);
self.emit(predefined, base_indent)?;
Expand Down Expand Up @@ -1245,4 +1297,24 @@ mod tests {
"<mo lspace=\"0.1667em\" rspace=\"0.1667em\">abc</mo>"
);
}

#[test]
fn render_enclose() {
let content = Node::Row {
nodes: &[
&Node::IdentifierChar('a', LetterAttr::Default),
&Node::IdentifierChar('b', LetterAttr::Default),
&Node::IdentifierChar('c', LetterAttr::Default),
],
attr: RowAttr::None,
};

assert_eq!(
render(&Node::Enclose {
content: &content,
notation: Notation::UP_DIAGONAL | Notation::DOWN_DIAGONAL
}),
"<menclose notation=\"updiagonalstrike downdiagonalstrike\"><mrow><mi>a</mi><mi>b</mi><mi>c</mi></mrow><mrow class=\"menclose-updiagonalstrike\"></mrow><mrow class=\"menclose-downdiagonalstrike\"></mrow></menclose>"
);
}
}
12 changes: 12 additions & 0 deletions math-core/src/mathml_renderer/attribute.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use bitflags::bitflags;
#[cfg(feature = "serde")]
use serde::Serialize;

Expand Down Expand Up @@ -125,6 +126,17 @@ pub enum RowAttr {
Color(u8, u8, u8),
}

bitflags! {
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct Notation: u8 {
const HORIZONTAL = 1;
const UP_DIAGONAL = 1 << 1;
const DOWN_DIAGONAL = 1 << 2;
}
}

// Transform of unicode characters.
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
Expand Down
28 changes: 28 additions & 0 deletions math-core/tests/snapshots/wiki_test__wiki119.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
source: math-core/tests/wiki_test.rs
expression: "\\cfrac{x}{1 + \\cfrac{\\cancel{y}} {\\cancel{y}}} = \\cfrac{x}{2}"
---
<math>
<mfrac displaystyle="true" scriptlevel="0" style="padding-top: 0.1667em">
<mi>x</mi>
<mrow>
<mn>1</mn>
<mo>+</mo>
<mfrac displaystyle="true" scriptlevel="0" style="padding-top: 0.1667em">
<menclose notation="updiagonalstrike">
<mi>y</mi>
<mrow class="menclose-updiagonalstrike"></mrow>
</menclose>
<menclose notation="updiagonalstrike">
<mi>y</mi>
<mrow class="menclose-updiagonalstrike"></mrow>
</menclose>
</mfrac>
</mrow>
</mfrac>
<mo>=</mo>
<mfrac displaystyle="true" scriptlevel="0" style="padding-top: 0.1667em">
<mi>x</mi>
<mn>2</mn>
</mfrac>
</math>
10 changes: 10 additions & 0 deletions math-core/tests/snapshots/wiki_test__wiki203.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: math-core/tests/wiki_test.rs
expression: "\\sout{q}"
---
<math>
<menclose notation="horizontalstrike">
<mi>q</mi>
<mrow class="menclose-horizontalstrike"></mrow>
</menclose>
</math>
11 changes: 7 additions & 4 deletions math-core/tests/wiki_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -906,8 +906,8 @@ fn wiki_test() {
}
}
assert_eq!(n_match, 10);
assert_eq!(n_diff, 182);
assert_eq!(n_fail, 26);
assert_eq!(n_diff, 184);
assert_eq!(n_fail, 24);
}

/// Prettify HTML input
Expand Down Expand Up @@ -1134,7 +1134,10 @@ fn test_nonfailing_wiki_tests() {
118,
r"\dfrac{2}{4} = 0.5 \qquad \dfrac{2}{c + \dfrac{2}{d + \dfrac{2}{4}}} = a",
),
// (119, r"\cfrac{x}{1 + \cfrac{\cancel{y}} {\cancel{y}}} = \cfrac{x}{2}"),
(
119,
r"\cfrac{x}{1 + \cfrac{\cancel{y}} {\cancel{y}}} = \cfrac{x}{2}",
),
(120, r"\binom{n}{k}"),
(121, r"\dbinom{n}{k}"),
(122, r"\begin{matrix} x & y \\ z & v \end{matrix}"),
Expand Down Expand Up @@ -1353,7 +1356,7 @@ fn test_nonfailing_wiki_tests() {
// (200, r"| \mathord\uparrow \rangle"),
(201, r"\wideparen{AB}"),
(202, r"\dddot{x}"),
// (203, r"\sout{q}"),
(203, r"\sout{q}"),
// (204, r"\mathrlap{\,/}{=}"),
// (205, r"\text{\textsf{textual description}}"),
(206, r"α π"),
Expand Down
Loading
Loading