Text

LaTeX maths

LaTeX maths to MathML through pulldown-latex: inline formulas, displayed blocks and whole texts with $...$ in them, drawn by browsers with no script, and mistakes placed by line and column.

Version 1.0.0 · 5 functions · 2 crates

#math#mathml#latex#markup

Functions

What use latex_math.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use latex_math_mathml, latex_math_block from latex_math (Quillo 0.6).

latex_math_mathml(latex: str) → stror an error

A formula as inline MathML: latex_math_mathml("x^2"). A mistake is an error saying its line, column and what is near it.

latex_math_block(latex: str) → stror an error

A formula as a displayed block, with limits under sums and larger fractions.

latex_math_render(latex: str, options: any) → stror an error

With choices: { block, xml, annotate, style }. xml adds the namespace, for Word and other programs; annotate keeps the LaTeX inside the markup for copy and paste; style is tex, iso, french or upright.

latex_math_check(latex: str) → any

Whether a formula is good, without rendering: { ok, message, line, column, near }, for showing a mistake as someone types.

latex_math_text(text: str) → anyor an error

A text with $...$ and $$...$$ (also \(...\) and \[...\]) replaced by MathML; \$ is a dollar sign and the rest is left as it is, so it suits HTML or Markdown on its way to HTML. A formula with a mistake is an error giving its place in the text; $ left open too. Gives { html, formulas }.

Get it

$ quillo install latex_math               # latex_math.qwer into this folder$ quillo install latex_math --example     # and its example, latex_math.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/latex_math.qwer$ curl -O https://quillolang.org/plugins/files/latex_math.qwe

Then, in your program, all of it or only what you call:

main.qwe
use latex_math.qweruse latex_math_mathml, latex_math_block from latex_math      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
serde_json = { version = "1", features = ["preserve_order"] }pulldown-latex = "=0.8.0"

Example

latex_math.qwe
// Formulas for a page: LaTeX turned into MathML, which browsers draw with no script or font of// their own; a text with dollar signs in it done whole; and a mistake placed for the writer.////   quillo run latex_math.qwe      then open http://localhost:8080/page, /formula, /options, /text and /mistakesuse "latex_math.qwer"let QUADRATIC = "x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}"route GET /page = respond(200, `<!doctype html><title>Roots</title><p>The roots are ${latex_math_block(QUADRATIC)?}</p>`, { "content-type": "text/html" })route GET /formula = {    inline: latex_math_mathml("x^2 + y^2 = z^2")?,    sum: latex_math_block("\\sum_{i=1}^n i = \\frac{n(n+1)}{2}")?,    matrix: latex_math_block("\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}")?,}route GET /options = {    xml: latex_math_render("a<b", { xml: true })?,    source_kept: latex_math_render("E=mc^2", { annotate: true })?,    upright: latex_math_render("x", { style: "upright" })?,    wrong_style: latex_math_render("x", { style: "gothic" }).error,}// Characters that mean something in markup stay text.route GET /escaped = {    less: latex_math_mathml("a<b")?,    more: latex_math_mathml("a>b")?,    amp: latex_math_mathml("a \\& b")?,    not_less: latex_math_mathml("a \\not< b")?,    words: latex_math_mathml("\\text{a<b & c}")?,    name: latex_math_mathml("\\operatorname{<img src=x onerror=alert(1)>}")?,    source: latex_math_render("a<b", { annotate: true })?,}route GET /text = latex_math_text("Einstein's $E=mc^2$ cost me \\$5.\n\nAnd a block: $$\\int_0^1 x\\,dx$$ and \\(a+b\\) too.")?route GET /mistakes = {    unclosed: latex_math_check("\\frac{a}{b"),    unknown: latex_math_check("x + \\notacommand y"),    second_line: latex_math_check("a + b \\\\\nc ^ ^ d"),    good: latex_math_check("a^2").ok,    refused: latex_math_mathml("x^2^3").error,    open_dollar: latex_math_text("price: $5").error,    in_text: latex_math_text("First line.\nThen $x^$ fails.").error,}
$ quillo run latex_math.qwe$ curl 'localhost:8080/page'$ curl 'localhost:8080/formula'$ curl 'localhost:8080/escaped'$ curl 'localhost:8080/options'$ curl 'localhost:8080/text'$ curl 'localhost:8080/mistakes'

Source

latex_math.qwer
//! LaTeX maths to MathML, through pulldown-latex: formulas and whole texts with $...$ turned into markup browsers draw on their own, and errors placed by line and column.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }//! pulldown-latex = "=0.8.0"use pulldown_latex::config::{DisplayMode, MathStyle};use pulldown_latex::event::{Content, Event};use pulldown_latex::{push_mathml, Parser, ParserError, RenderConfig, Storage};use serde_json::{json, Value};const PAD: usize = 16;/// Errors at the end of the formula, which the parser places at its start.const AT_END: &[&str] = &["expected a token", "unbalanced group found, expected it to be closed"];/// The parser's context shows 12 bytes either side of a fault, so padding the formula with spaces, which maths ignores, makes the fault's byte fall at a known place in that context.fn locate(latex: &str) -> Option<usize> {    let flat: String = latex.chars().map(|c| if c == '\n' || c == '\r' { ' ' } else { c }).collect();    let padded = format!("{}{}{}", " ".repeat(PAD), flat, " ".repeat(PAD));    let storage = Storage::new();    let err = Parser::new(&padded, &storage).find_map(Result::err)?.to_string();    let snippet: String = err.lines().skip_while(|l| !l.contains("context:")).nth(2)?.chars().skip(1).skip(1).collect();    let snippet = snippet.trim_end_matches('\u{a0}');    let at = padded.find(snippet).or_else(|| padded.find(snippet.trim()))?;    let spot = (at + 12).saturating_sub(PAD).min(latex.len());    // The parser stands just past what it could not read: step back over any space.    Some(latex[..latex.floor_char_boundary(spot)].trim_end().len())}fn place(latex: &str, at: usize) -> (usize, usize) {    let mut end = at.min(latex.len());    while !latex.is_char_boundary(end) {        end -= 1;    }    let before = &latex[..end];    (before.matches('\n').count() + 1, before.rsplit('\n').next().unwrap_or("").chars().count() + 1)}fn problem(latex: &str) -> Option<Value> {    let storage = Storage::new();    let full = Parser::new(latex, &storage).find_map(Result::err)?.to_string();    let message = full.lines().next().unwrap_or("").trim_start_matches("parsing error: ").to_string();    let at = if AT_END.iter().any(|m| message.starts_with(m)) { Some(latex.trim_end().len()) } else { locate(latex) };    let (line, column) = match at {        Some(a) => { let p = place(latex, a); (json!(p.0), json!(p.1)) }        None => (Value::Null, Value::Null),    };    let near = at.map(|a| {        let start = latex.char_indices().map(|x| x.0).filter(|i| *i + 12 >= a).next().unwrap_or(0);        latex[start..].chars().take(24).collect::<String>().replace('\n', " ")    });    Some(json!({ "message": message, "line": line, "column": column, "near": near }))}/// pulldown-latex writes an operator's character and a function's name as they are: `a<b` gives `<mo><</mo>`, which is no XML, and `\\operatorname{<img ...>}` would be markup. Names go through as text, which it escapes.fn escaped<'a>(e: Result<Event<'a>, ParserError>) -> Result<Event<'a>, ParserError> {    match e {        Ok(Event::Content(Content::Function(s))) if s.contains(['<', '>', '&', '"']) => Ok(Event::Content(Content::Text(s))),        o => o,    }}fn clean_operators(x: String) -> String {    let mut out = x;    for (raw, safe) in [("&", "&amp;"), ("<", "&lt;"), (">", "&gt;")] {        for (open, tail) in [(">", "</m"), (">", "\u{338}</m")] {            let bad = format!("{}{}{}", open, raw, tail);            if out.contains(&bad) {                out = out.replace(&bad, &format!("{}{}{}", open, safe, tail));            }        }    }    out}fn say(p: &Value) -> String {    match p["line"].as_u64() {        Some(l) => format!("line {}, column {}: {}, near `{}`", l, p["column"], p["message"].as_str().unwrap_or(""), p["near"].as_str().unwrap_or("").trim()),        None => p["message"].as_str().unwrap_or("").to_string(),    }}fn style(name: &str) -> Result<MathStyle, String> {    Ok(match name {        "" | "tex" => MathStyle::TeX,        "iso" => MathStyle::ISO,        "french" => MathStyle::French,        "upright" => MathStyle::Upright,        o => return Err(format!("math style `{}` should be tex, iso, french or upright", o)),    })}fn render(latex: &str, block: bool, xml: bool, annotate: bool, math_style: MathStyle) -> Result<String, String> {    if let Some(p) = problem(latex) {        return Err(say(&p));    }    let storage = Storage::new();    let parser = Parser::new(latex, &storage);    let mut out = String::new();    // The annotation is written as given: escaped here, or `</annotation>` in a formula would end it.    let source = latex.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;");    let config = RenderConfig { display_mode: if block { DisplayMode::Block } else { DisplayMode::Inline }, annotation: if annotate { Some(&source) } else { None }, error_color: (178, 34, 34), xml, math_style };    push_mathml(&mut out, parser.map(escaped), config).map_err(|e| e.to_string())?;    Ok(clean_operators(out))}/// A formula as inline MathML: `latex_math_mathml("x^2")`. A mistake is an error saying its line, column and what is near it.pub fn latex_math_mathml(latex: &str) -> Result<String, String> {    render(latex, false, false, false, MathStyle::TeX)}/// A formula as a displayed block, with limits under sums and larger fractions.pub fn latex_math_block(latex: &str) -> Result<String, String> {    render(latex, true, false, false, MathStyle::TeX)}/// With choices: `{ block, xml, annotate, style }`. `xml` adds the namespace, for Word and other programs; `annotate` keeps the LaTeX inside the markup for copy and paste; `style` is tex, iso, french or upright.pub fn latex_math_render(latex: &str, options: serde_json::Value) -> Result<String, String> {    let b = |k: &str| options[k].as_bool().unwrap_or(false);    render(latex, b("block"), b("xml"), b("annotate"), style(options["style"].as_str().unwrap_or(""))?)}/// Whether a formula is good, without rendering: `{ ok, message, line, column, near }`, for showing a mistake as someone types.pub fn latex_math_check(latex: &str) -> Value {    match problem(latex) {        None => json!({ "ok": true, "message": null, "line": null, "column": null, "near": null }),        Some(mut p) => { p["ok"] = json!(false); p }    }}/// A text with `$...$` and `$$...$$` (also `\(...\)` and `\[...\]`) replaced by MathML; `\$` is a dollar sign and the rest is left as it is, so it suits HTML or Markdown on its way to HTML. A formula with a mistake is an error giving its place in the text; `$` left open too. Gives `{ html, formulas }`.pub fn latex_math_text(text: &str) -> Result<Value, String> {    let mut out = String::with_capacity(text.len());    let mut rest = text;    let mut pos = 0usize;    let mut formulas = 0;    loop {        let open = [("\\$", "", false), ("$$", "$$", true), ("$", "$", false), ("\\(", "\\)", false), ("\\[", "\\]", true)];        let found = open.iter().filter_map(|o| rest.find(o.0).map(|i| (i, *o))).min_by_key(|x| (x.0, std::cmp::Reverse(x.1 .0.len())));        let Some((i, (start, end, block))) = found else {            out.push_str(rest);            break;        };        out.push_str(&rest[..i]);        let body = &rest[i + start.len()..];        pos += i + start.len();        if start == "\\$" {            out.push('$');            rest = body;            continue;        }        let mut search = 0;        let close = loop {            match body[search..].find(end) {                None => {                    let (l, c) = place(text, pos - start.len());                    return Err(format!("line {}, column {}: `{}` is never closed", l, c, start));                }                Some(k) if k + search > 0 && body.as_bytes()[k + search - 1] == b'\\' => search += k + 1,                Some(k) => break k + search,            }        };        let formula = &body[..close];        if let Some(p) = problem(formula) {            let (l, c) = place(text, pos);            let (fl, fc) = (p["line"].as_u64().unwrap_or(1) as usize, p["column"].as_u64().unwrap_or(1) as usize);            let (line, column) = if fl == 1 { (l, c + fc - 1) } else { (l + fl - 1, fc) };            return Err(format!("line {}, column {}: {}, near `{}`", line, column, p["message"].as_str().unwrap_or(""), p["near"].as_str().unwrap_or("").trim()));        }        let html = render(formula, block, false, false, MathStyle::TeX)?;        out.push_str(&html);        formulas += 1;        rest = &body[close + end.len()..];        pos += close + end.len();    }    Ok(json!({ "html": out, "formulas": formulas }))}
Thread
48Useful? Like it, or add to its thread below.

Thread 0 posts

On LaTeX maths: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.