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
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 errorA 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 errorA formula as a displayed block, with limits under sums and larger fractions.
latex_math_render(latex: str, options: any) → stror an errorWith 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) → anyWhether 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 errorA 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/latex_math.qwer$ curl -O https://quillolang.org/plugins/files/latex_math.qweThen, in your program, all of it or only what you call:
use latex_math.qweruse latex_math_mathml, latex_math_block from latex_math // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }pulldown-latex = "=0.8.0"Example
// 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 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 [("&", "&"), ("<", "<"), (">", ">")] { 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('&', "&").replace('<', "<").replace('>', ">"); 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 }))}Related
asciidoc.qwer · 1.0.0AsciiDoc to HTML: headings with ids, attributes and conditionals, nested lists with continuation, description lists, tables with spans, admonitions, source blocks with callouts, quotes, images, footnotes, a table of contents. Text is escaped and `javascript:` links dropped, raw passthrough refused unless asked for, and includes refused or kept inside one folder, with line ranges and tags.6 views · 11 downloadsreStructuredText rst.qwer · 1.0.0reStructuredText to HTML with no Python: sections by adornment in the order they appear, nested lists, definition and field lists, literal blocks, grid tables with spans, simple, list and CSV tables, directives, roles, targets of every kind, footnotes and substitutions. Raw blocks and includes refused, and references that lead nowhere listed.3 views · 6 downloadsThread 0 posts
On LaTeX maths: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.