fix: handle latex equations when rendering typst
This commit is contained in:
+113
-7
@@ -139,10 +139,25 @@ pub fn to_markdown(src: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Passes authoring markup through for Typst.
|
||||
/// Passes authoring markup through for Typst, translating inline LaTeX math on
|
||||
/// the way.
|
||||
///
|
||||
/// The markup is already a Typst subset, so this only normalizes whitespace and
|
||||
/// escapes the few characters Typst treats specially in content mode.
|
||||
/// Outside math this only escapes the characters Typst treats specially in
|
||||
/// content mode: a bare `@` or `<` starts a reference or label.
|
||||
///
|
||||
/// Math is different. Authors write ordinary LaTeX between `$...$`, and Typst's
|
||||
/// own math grammar is not LaTeX's — a backslash escapes the next character
|
||||
/// rather than naming a symbol, so `\Delta`, `\times`, `\ln` compile without
|
||||
/// error and print wrong. Each `$...$` span is instead handed whole to
|
||||
/// mitex's `mi`, which parses LaTeX grammar on purpose: `$\phi$` becomes
|
||||
/// `#mi("\\phi")`. The `@`/`<`/`>` escaping above is skipped for anything
|
||||
/// inside the span, since it reaches Typst as a string argument, not as
|
||||
/// markup — escaping `<` there would corrupt the LaTeX rather than protect
|
||||
/// anything.
|
||||
///
|
||||
/// `\$` is left alone, matching LaTeX's own convention for a literal dollar
|
||||
/// sign. A `$` with no matching close is escaped the same way rather than left
|
||||
/// to open Typst's own math mode on a stray price or a malformed source line.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -152,17 +167,84 @@ pub fn to_markdown(src: &str) -> String {
|
||||
///
|
||||
/// Typst content-mode markup.
|
||||
pub fn to_typst(src: &str) -> String {
|
||||
let src = src.trim();
|
||||
let chars: Vec<(usize, char)> = src.char_indices().collect();
|
||||
let mut out = String::with_capacity(src.len());
|
||||
for ch in src.trim().chars() {
|
||||
match ch {
|
||||
// A bare `@` or `<` starts a Typst reference or label.
|
||||
let mut i = 0;
|
||||
while i < chars.len() {
|
||||
let (_, c) = chars[i];
|
||||
|
||||
// An escaped pair is copied verbatim and never reconsidered, so `\$`
|
||||
// can't be mistaken for the start of math and an `\@`/`\<`/`\>` an
|
||||
// author already wrote is not escaped a second time.
|
||||
if c == '\\' && i + 1 < chars.len() {
|
||||
out.push('\\');
|
||||
out.push(chars[i + 1].1);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '$' {
|
||||
match find_math_close(&chars, i) {
|
||||
Some(close) => {
|
||||
let start = chars[i + 1].0;
|
||||
let end = chars[close].0;
|
||||
out.push_str("#mi(");
|
||||
push_typst_string(&mut out, &src[start..end]);
|
||||
out.push(')');
|
||||
i = close + 1;
|
||||
continue;
|
||||
}
|
||||
None => {
|
||||
out.push_str("\\$");
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match c {
|
||||
'@' => out.push_str("\\@"),
|
||||
'<' => out.push_str("\\<"),
|
||||
'>' => out.push_str("\\>"),
|
||||
_ => out.push(c),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Finds the index into `chars` of the `$` matching the opener at `open`. A
|
||||
/// backslash-escaped pair is skipped as a unit, so a `\$` inside the math span
|
||||
/// doesn't close it early.
|
||||
fn find_math_close(chars: &[(usize, char)], open: usize) -> Option<usize> {
|
||||
let mut j = open + 1;
|
||||
while j < chars.len() {
|
||||
match chars[j].1 {
|
||||
'\\' if j + 1 < chars.len() => j += 2,
|
||||
'$' => return Some(j),
|
||||
_ => j += 1,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Writes `s` as a quoted Typst string. Kept local, duplicating the five-case
|
||||
/// match in `typst::value::write_string`, rather than reaching into the
|
||||
/// Typst-specific value writer for one small helper.
|
||||
fn push_typst_string(out: &mut String, s: &str) {
|
||||
out.push('"');
|
||||
for ch in s.chars() {
|
||||
match ch {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
_ => out.push(ch),
|
||||
}
|
||||
}
|
||||
out
|
||||
out.push('"');
|
||||
}
|
||||
|
||||
/// Escapes the five XML-significant characters.
|
||||
@@ -423,3 +505,27 @@ mod tests {
|
||||
assert_eq!(to_markdown("one\n\ntwo"), "one\n\ntwo");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latex_math_becomes_a_mitex_call() {
|
||||
assert_eq!(
|
||||
to_typst("angle $\\phi$ (phi)"),
|
||||
"angle #mi(\"\\\\phi\") (phi)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comparison_operators_inside_math_are_not_escaped() {
|
||||
assert_eq!(to_typst("$\\Delta H < 0$"), "#mi(\"\\\\Delta H < 0\")");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_starters_outside_math_are_still_escaped() {
|
||||
assert_eq!(to_typst("see @fig:x and x < y"), "see \\@fig:x and x \\< y");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaped_and_unmatched_dollar_signs_are_left_or_escaped() {
|
||||
assert_eq!(to_typst("costs \\$5 total"), "costs \\$5 total");
|
||||
assert_eq!(to_typst("just $5"), "just \\$5");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user