2022-12-06 12:37:08 +01:00

368 lines
8.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Mathematical formulas.
mod tex;
use typst::model::{Guard, SequenceNode};
use unicode_segmentation::UnicodeSegmentation;
use self::tex::layout_tex;
use crate::prelude::*;
use crate::text::{FontFamily, LinebreakNode, SpaceNode, SymbolNode, TextNode};
/// A piece of a mathematical formula.
#[derive(Debug, Clone, Hash)]
pub struct MathNode {
/// The pieces of the formula.
pub children: Vec<Content>,
/// Whether the formula is display-level.
pub display: bool,
}
#[node(Show, Layout, Inline, Texify)]
impl MathNode {
fn field(&self, name: &str) -> Option<Value> {
match name {
"display" => Some(Value::Bool(self.display)),
_ => None,
}
}
}
impl Show for MathNode {
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
let mut map = StyleMap::new();
map.set_family(FontFamily::new("NewComputerModernMath"), styles);
let mut realized = self
.clone()
.pack()
.guarded(Guard::Base(NodeId::of::<Self>()))
.styled_with_map(map);
if self.display {
realized = realized.aligned(Axes::with_x(Some(Align::Center.into())))
}
Ok(realized)
}
}
impl Layout for MathNode {
fn layout(
&self,
vt: &mut Vt,
styles: StyleChain,
_: &Regions,
) -> SourceResult<Fragment> {
let mut t = Texifier::new();
self.texify(&mut t)?;
layout_tex(vt, &t.finish(), self.display, styles)
}
}
impl Inline for MathNode {}
/// Turn a math node into TeX math code.
#[capability]
trait Texify {
/// Perform the conversion.
fn texify(&self, t: &mut Texifier) -> SourceResult<()>;
/// Texify the node, but trim parentheses..
fn texify_unparen(&self, t: &mut Texifier) -> SourceResult<()> {
let s = {
let mut sub = Texifier::new();
self.texify(&mut sub)?;
sub.finish()
};
let unparened = if s.starts_with("\\left(") && s.ends_with("\\right)") {
s[6..s.len() - 7].into()
} else {
s
};
t.push_str(&unparened);
Ok(())
}
}
/// Builds the TeX representation of the formula.
struct Texifier {
tex: EcoString,
support: bool,
space: bool,
}
impl Texifier {
/// Create a new texifier.
fn new() -> Self {
Self {
tex: EcoString::new(),
support: false,
space: false,
}
}
/// Finish texifier and return the TeX string.
fn finish(self) -> EcoString {
self.tex
}
/// Push a weak space.
fn push_space(&mut self) {
self.space = !self.tex.is_empty();
}
/// Mark this position as supportive. This allows a space before or after
/// to exist.
fn support(&mut self) {
self.support = true;
}
/// Flush a space.
fn flush(&mut self) {
if self.space && self.support {
self.tex.push_str("\\ ");
}
self.space = false;
self.support = false;
}
/// Push a string.
fn push_str(&mut self, s: &str) {
self.flush();
self.tex.push_str(s);
}
/// Escape and push a char for TeX usage.
#[rustfmt::skip]
fn push_escaped(&mut self, c: char) {
self.flush();
match c {
' ' => self.tex.push_str("\\ "),
'%' | '&' | '$' | '#' => {
self.tex.push('\\');
self.tex.push(c);
self.tex.push(' ');
}
'{' => self.tex.push_str("\\left\\{"),
'}' => self.tex.push_str("\\right\\}"),
'[' | '(' => {
self.tex.push_str("\\left");
self.tex.push(c);
}
']' | ')' => {
self.tex.push_str("\\right");
self.tex.push(c);
}
'a' ..= 'z' | 'A' ..= 'Z' | '0' ..= '9' | 'Α' ..= 'Ω' | 'α' ..= 'ω' |
'*' | '+' | '-' | '?' | '!' | '=' | '<' | '>' |
':' | ',' | ';' | '|' | '/' | '@' | '.' | '"' => self.tex.push(c),
c => {
if let Some(sym) = unicode_math::SYMBOLS
.iter()
.find(|sym| sym.codepoint == c) {
self.tex.push('\\');
self.tex.push_str(sym.name);
self.tex.push(' ');
}
}
}
}
}
impl Texify for MathNode {
fn texify(&self, t: &mut Texifier) -> SourceResult<()> {
for child in &self.children {
child.texify(t)?;
}
Ok(())
}
}
impl Texify for Content {
fn texify(&self, t: &mut Texifier) -> SourceResult<()> {
if self.is::<SpaceNode>() {
t.push_space();
return Ok(());
}
if self.is::<LinebreakNode>() {
t.push_str("\\");
return Ok(());
}
if let Some(node) = self.to::<SymbolNode>() {
if let Some(c) = symmie::get(&node.0) {
t.push_escaped(c);
return Ok(());
} else if let Some(span) = self.span() {
bail!(span, "unknown symbol");
}
}
if let Some(node) = self.to::<TextNode>() {
t.support();
t.push_str("\\mathrm{");
for c in node.0.chars() {
t.push_escaped(c);
}
t.push_str("}");
t.support();
return Ok(());
}
if let Some(node) = self.to::<SequenceNode>() {
for child in &node.0 {
child.texify(t)?;
}
return Ok(());
}
if let Some(node) = self.with::<dyn Texify>() {
return node.texify(t);
}
if let Some(span) = self.span() {
bail!(span, "not allowed here");
}
Ok(())
}
}
/// An atom in a math formula: `x`, `+`, `12`.
#[derive(Debug, Hash)]
pub struct AtomNode(pub EcoString);
#[node(Texify)]
impl AtomNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("text")?).pack())
}
}
impl Texify for AtomNode {
fn texify(&self, t: &mut Texifier) -> SourceResult<()> {
let multi = self.0.graphemes(true).count() > 1;
if multi {
t.push_str("\\mathrm{");
}
for c in self.0.chars() {
let supportive = c == '|';
if supportive {
t.support();
}
t.push_escaped(c);
if supportive {
t.support();
}
}
if multi {
t.push_str("}");
}
Ok(())
}
}
/// A fraction in a mathematical formula.
#[derive(Debug, Hash)]
pub struct FracNode {
/// The numerator.
pub num: Content,
/// The denominator.
pub denom: Content,
}
#[node(Texify)]
impl FracNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let num = args.expect("numerator")?;
let denom = args.expect("denominator")?;
Ok(Self { num, denom }.pack())
}
}
impl Texify for FracNode {
fn texify(&self, t: &mut Texifier) -> SourceResult<()> {
t.push_str("\\frac{");
self.num.texify_unparen(t)?;
t.push_str("}{");
self.denom.texify_unparen(t)?;
t.push_str("}");
Ok(())
}
}
/// A sub- and/or superscript in a mathematical formula.
#[derive(Debug, Hash)]
pub struct ScriptNode {
/// The base.
pub base: Content,
/// The subscript.
pub sub: Option<Content>,
/// The superscript.
pub sup: Option<Content>,
}
#[node(Texify)]
impl ScriptNode {}
impl Texify for ScriptNode {
fn texify(&self, t: &mut Texifier) -> SourceResult<()> {
self.base.texify(t)?;
if let Some(sub) = &self.sub {
t.push_str("_{");
sub.texify_unparen(t)?;
t.push_str("}");
}
if let Some(sup) = &self.sup {
t.push_str("^{");
sup.texify_unparen(t)?;
t.push_str("}");
}
Ok(())
}
}
/// A math alignment indicator: `&`, `&&`.
#[derive(Debug, Hash)]
pub struct AlignNode(pub usize);
#[node(Texify)]
impl AlignNode {}
impl Texify for AlignNode {
fn texify(&self, _: &mut Texifier) -> SourceResult<()> {
Ok(())
}
}
/// A square root node.
#[derive(Debug, Hash)]
pub struct SqrtNode(Content);
#[node(Texify)]
impl SqrtNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
}
impl Texify for SqrtNode {
fn texify(&self, t: &mut Texifier) -> SourceResult<()> {
t.push_str("\\sqrt{");
self.0.texify_unparen(t)?;
t.push_str("}");
Ok(())
}
}