katana-document-viewer 0.1.4

KatanA document viewer artifact, render evaluation, and export foundation.
Documentation
pub(crate) struct SurfaceMathText;

impl SurfaceMathText {
    pub(crate) fn render(expression: &str) -> String {
        MathRenderer::new(expression.trim()).render()
    }
}

struct MathRenderer<'a> {
    remaining: &'a str,
    output: String,
}

impl<'a> MathRenderer<'a> {
    fn new(expression: &'a str) -> Self {
        Self {
            remaining: expression,
            output: String::new(),
        }
    }

    fn render(mut self) -> String {
        while !self.remaining.is_empty() {
            if self.try_render_fraction() {
                continue;
            }
            if self.try_render_simple_token() {
                continue;
            }
            self.render_next_character();
        }
        self.output.split_whitespace().collect::<Vec<_>>().join(" ")
    }

    fn try_render_fraction(&mut self) -> bool {
        let Some(rest) = self.remaining.strip_prefix(r"\frac") else {
            return false;
        };
        let Some((numerator, after_numerator)) = take_braced(rest) else {
            return false;
        };
        let Some((denominator, after_denominator)) = take_braced(after_numerator) else {
            return false;
        };

        self.output.push('(');
        self.output.push_str(&render_inline_text(numerator));
        self.output.push_str(")⁄(");
        self.output.push_str(&render_inline_text(denominator));
        self.output.push(')');
        self.remaining = after_denominator;
        true
    }

    fn try_render_simple_token(&mut self) -> bool {
        if self.try_render_static_token(r"\int", '') {
            return true;
        }
        if self.try_render_static_token(r"\sum", '') {
            return true;
        }
        if self.try_render_static_token(r"\,", ' ') {
            return true;
        }
        if self.try_render_script('^', true) {
            return true;
        }
        if self.try_render_script('_', false) {
            return true;
        }
        false
    }

    fn try_render_static_token(&mut self, token: &str, replacement: char) -> bool {
        let Some(rest) = self.remaining.strip_prefix(token) else {
            return false;
        };
        self.output.push(replacement);
        self.remaining = rest;
        true
    }

    fn try_render_script(&mut self, marker: char, is_superscript: bool) -> bool {
        let Some(rest) = self.remaining.strip_prefix(marker) else {
            return false;
        };
        let (script, next) = take_script(rest);
        let mapped = if is_superscript {
            script.chars().map(superscript).collect::<String>()
        } else {
            script.chars().map(subscript).collect::<String>()
        };
        self.output.push_str(&mapped);
        self.remaining = next;
        true
    }

    fn render_next_character(&mut self) {
        let mut characters = self.remaining.chars();
        let Some(character) = characters.next() else {
            self.remaining = "";
            return;
        };
        if character != '\\' {
            self.output.push(character);
        }
        self.remaining = characters.as_str();
    }
}

fn render_inline_text(expression: &str) -> String {
    MathRenderer::new(expression).render()
}

fn take_braced(text: &str) -> Option<(&str, &str)> {
    let rest = text.strip_prefix('{')?;
    let mut depth = 0usize;
    for (index, character) in rest.char_indices() {
        match character {
            '{' => depth += 1,
            '}' if depth == 0 => return Some((&rest[..index], &rest[index + 1..])),
            '}' => depth -= 1,
            _ => {}
        }
    }
    None
}

fn take_script(text: &str) -> (&str, &str) {
    if let Some((script, rest)) = take_braced(text) {
        return (script, rest);
    }
    let Some(character) = text.chars().next() else {
        return ("", text);
    };
    let end = character.len_utf8();
    (&text[..end], &text[end..])
}

fn superscript(character: char) -> char {
    match character {
        '0' => '',
        '1' => '¹',
        '2' => '²',
        '3' => '³',
        '4' => '',
        '5' => '',
        '6' => '',
        '7' => '',
        '8' => '',
        '9' => '',
        '+' => '',
        '-' => '',
        '=' => '',
        'n' => '',
        'x' => 'ˣ',
        _ => character,
    }
}

fn subscript(character: char) -> char {
    match character {
        '0' => '',
        '1' => '',
        '2' => '',
        '3' => '',
        '4' => '',
        '5' => '',
        '6' => '',
        '7' => '',
        '8' => '',
        '9' => '',
        '+' => '',
        '-' => '',
        '=' => '',
        'k' => '',
        _ => character,
    }
}

#[cfg(test)]
#[path = "export_surface_math_tests.rs"]
mod tests;