synpad 0.1.0

A full-featured Matrix chat client built with Dioxus
/// Simple LaTeX/math expression renderer.
///
/// Detects `$...$` (inline) and `$$...$$` (display) math expressions
/// and converts them to HTML with appropriate CSS classes.
/// For full rendering, a WASM math library like KaTeX could be integrated.

/// Process text containing LaTeX math expressions and wrap them in HTML spans.
pub fn render_latex_in_text(text: &str) -> String {
    let mut result = String::with_capacity(text.len());
    let chars: Vec<char> = text.chars().collect();
    let len = chars.len();
    let mut i = 0;

    while i < len {
        // Check for display math ($$...$$)
        if i + 1 < len && chars[i] == '$' && chars[i + 1] == '$' {
            if let Some(end) = find_closing_dollars(&chars, i + 2, true) {
                let math: String = chars[i + 2..end].iter().collect();
                let rendered = render_math_expression(&math, true);
                result.push_str(&rendered);
                i = end + 2; // skip past closing $$
                continue;
            }
        }

        // Check for inline math ($...$)
        if chars[i] == '$' {
            if let Some(end) = find_closing_dollars(&chars, i + 1, false) {
                let math: String = chars[i + 1..end].iter().collect();
                // Only render if it contains actual math content (not just numbers)
                if math.len() > 1 && contains_math_chars(&math) {
                    let rendered = render_math_expression(&math, false);
                    result.push_str(&rendered);
                    i = end + 1; // skip past closing $
                    continue;
                }
            }
        }

        result.push(chars[i]);
        i += 1;
    }

    result
}

/// Find the closing dollar sign(s) for a math expression.
fn find_closing_dollars(chars: &[char], start: usize, double: bool) -> Option<usize> {
    let len = chars.len();
    let mut i = start;
    while i < len {
        if double {
            if i + 1 < len && chars[i] == '$' && chars[i + 1] == '$' {
                return Some(i);
            }
        } else if chars[i] == '$' {
            return Some(i);
        }
        // Don't match escaped dollars
        if chars[i] == '\\' {
            i += 2;
            continue;
        }
        i += 1;
    }
    None
}

/// Check if a string contains math-like characters (operators, greek letters, etc.)
fn contains_math_chars(s: &str) -> bool {
    s.contains(|c: char| {
        matches!(c, '+' | '-' | '*' | '/' | '=' | '<' | '>' | '^' | '_' | '{' | '}'
                | '\\' | '(' | ')' | '[' | ']' | '|' | '' | '' | '' | ''
                | 'α' | 'β' | 'γ' | 'δ' | 'ε' | 'θ' | 'λ' | 'μ' | 'π' | 'σ' | 'φ' | 'ω')
    })
}

/// Render a math expression to HTML.
///
/// Converts common LaTeX commands to Unicode/HTML equivalents.
fn render_math_expression(expr: &str, display: bool) -> String {
    let mut html = expr.to_string();

    // Replace common LaTeX commands with Unicode
    let replacements = [
        ("\\alpha", "α"), ("\\beta", "β"), ("\\gamma", "γ"), ("\\delta", "δ"),
        ("\\epsilon", "ε"), ("\\zeta", "ζ"), ("\\eta", "η"), ("\\theta", "θ"),
        ("\\iota", "ι"), ("\\kappa", "κ"), ("\\lambda", "λ"), ("\\mu", "μ"),
        ("\\nu", "ν"), ("\\xi", "ξ"), ("\\pi", "π"), ("\\rho", "ρ"),
        ("\\sigma", "σ"), ("\\tau", "τ"), ("\\upsilon", "υ"), ("\\phi", "φ"),
        ("\\chi", "χ"), ("\\psi", "ψ"), ("\\omega", "ω"),
        ("\\Alpha", "Α"), ("\\Beta", "Β"), ("\\Gamma", "Γ"), ("\\Delta", "Δ"),
        ("\\Theta", "Θ"), ("\\Lambda", "Λ"), ("\\Pi", "Π"), ("\\Sigma", "Σ"),
        ("\\Phi", "Φ"), ("\\Psi", "Ψ"), ("\\Omega", "Ω"),
        ("\\infty", ""), ("\\infinity", ""),
        ("\\sum", ""), ("\\prod", ""), ("\\int", ""),
        ("\\partial", ""), ("\\nabla", ""),
        ("\\pm", "±"), ("\\mp", ""), ("\\times", "×"), ("\\div", "÷"),
        ("\\cdot", "·"), ("\\ldots", ""), ("\\cdots", ""),
        ("\\leq", ""), ("\\geq", ""), ("\\neq", ""),
        ("\\approx", ""), ("\\equiv", ""), ("\\sim", ""),
        ("\\subset", ""), ("\\supset", ""), ("\\subseteq", ""), ("\\supseteq", ""),
        ("\\in", ""), ("\\notin", ""),
        ("\\cup", ""), ("\\cap", ""),
        ("\\forall", ""), ("\\exists", ""),
        ("\\neg", "¬"), ("\\land", ""), ("\\lor", ""),
        ("\\to", ""), ("\\gets", ""), ("\\leftrightarrow", ""),
        ("\\Rightarrow", ""), ("\\Leftarrow", ""), ("\\Leftrightarrow", ""),
        ("\\sqrt", ""), ("\\langle", ""), ("\\rangle", ""),
        ("\\lceil", ""), ("\\rceil", ""), ("\\lfloor", ""), ("\\rfloor", ""),
        ("\\emptyset", ""), ("\\varnothing", ""),
        ("\\mathbb{R}", ""), ("\\mathbb{Z}", ""), ("\\mathbb{N}", ""),
        ("\\mathbb{Q}", ""), ("\\mathbb{C}", ""),
    ];

    for (cmd, unicode) in replacements {
        html = html.replace(cmd, unicode);
    }

    // Handle superscripts: ^{...} -> <sup>...</sup>
    html = replace_braced_command(&html, "^", "sup");
    // Handle subscripts: _{...} -> <sub>...</sub>
    html = replace_braced_command(&html, "_", "sub");
    // Handle \frac{a}{b} -> a/b
    html = render_fractions(&html);
    // Handle \text{...} -> ...
    html = strip_command(&html, "\\text");
    html = strip_command(&html, "\\mathrm");
    html = strip_command(&html, "\\mathbf");

    // Clean up remaining backslash commands we don't recognize
    // (leave them as-is for readability)

    let class = if display { "math-display" } else { "math-inline" };
    let tag = if display { "div" } else { "span" };
    format!("<{tag} class=\"{class}\">{html}</{tag}>")
}

/// Replace ^{content} or _{content} with <tag>content</tag>.
fn replace_braced_command(text: &str, prefix: &str, tag: &str) -> String {
    let mut result = String::new();
    let chars: Vec<char> = text.chars().collect();
    let prefix_char = prefix.chars().next().unwrap();
    let mut i = 0;

    while i < chars.len() {
        if chars[i] == prefix_char && i + 1 < chars.len() && chars[i + 1] == '{' {
            // Find matching closing brace
            if let Some(end) = find_closing_brace(&chars, i + 2) {
                let content: String = chars[i + 2..end].iter().collect();
                result.push_str(&format!("<{tag}>{content}</{tag}>"));
                i = end + 1;
                continue;
            }
        }
        // Handle single character super/subscript: ^x or _x
        if chars[i] == prefix_char && i + 1 < chars.len() && chars[i + 1] != '{' && chars[i + 1].is_alphanumeric() {
            result.push_str(&format!("<{tag}>{}</{tag}>", chars[i + 1]));
            i += 2;
            continue;
        }
        result.push(chars[i]);
        i += 1;
    }

    result
}

fn find_closing_brace(chars: &[char], start: usize) -> Option<usize> {
    let mut depth = 1;
    let mut i = start;
    while i < chars.len() {
        match chars[i] {
            '{' => depth += 1,
            '}' => {
                depth -= 1;
                if depth == 0 {
                    return Some(i);
                }
            }
            _ => {}
        }
        i += 1;
    }
    None
}

/// Render \frac{a}{b} as (a)/(b).
fn render_fractions(text: &str) -> String {
    let mut result = text.to_string();
    while let Some(pos) = result.find("\\frac{") {
        let chars: Vec<char> = result.chars().collect();
        let start = pos + 6; // after \frac{
        if let Some(mid) = find_closing_brace(&chars, start) {
            let num: String = chars[start..mid].iter().collect();
            if mid + 1 < chars.len() && chars[mid + 1] == '{' {
                if let Some(end) = find_closing_brace(&chars, mid + 2) {
                    let den: String = chars[mid + 2..end].iter().collect();
                    let before: String = chars[..pos].iter().collect();
                    let after: String = chars[end + 1..].iter().collect();
                    result = format!("{before}<span class=\"math-frac\"><span class=\"math-num\">{num}</span><span class=\"math-den\">{den}</span></span>{after}");
                    continue;
                }
            }
        }
        break; // avoid infinite loop on malformed input
    }
    result
}

/// Strip a LaTeX command, keeping its braced argument.
fn strip_command(text: &str, cmd: &str) -> String {
    let mut result = text.to_string();
    let pattern = format!("{cmd}{{");
    while let Some(pos) = result.find(&pattern) {
        let chars: Vec<char> = result.chars().collect();
        let start = pos + pattern.len();
        if let Some(end) = find_closing_brace(&chars, start) {
            let content: String = chars[start..end].iter().collect();
            let before: String = chars[..pos].iter().collect();
            let after: String = chars[end + 1..].iter().collect();
            result = format!("{before}{content}{after}");
            continue;
        }
        break;
    }
    result
}