fix: handle latex equations when rendering typst
This commit is contained in:
@@ -607,6 +607,8 @@ defaults:
|
||||
extra:
|
||||
accent: '#017ab9'
|
||||
font: 'Libertinus Serif'
|
||||
show-bubbles: true
|
||||
bubble-radius: '0.42em'
|
||||
|
||||
variants:
|
||||
exam:
|
||||
|
||||
@@ -1070,8 +1070,61 @@ fn numeric_map(map: &BTreeMap<String, f64>) -> Value {
|
||||
)
|
||||
}
|
||||
|
||||
/// Emits authored markup as either a content block or a quoted string.
|
||||
/// Rewrites inline LaTeX math (`$...$`) into a call to mitex's `mi`, so Typst's
|
||||
/// own math grammar never has to parse it. Typst's `\times`, `\Delta`, `\ln` and
|
||||
/// friends are not valid Typst math — a bare backslash escapes the next
|
||||
/// character instead of naming a symbol — which is why equations compile but
|
||||
/// print wrong instead of failing outright. `\$` is left alone, matching LaTeX's
|
||||
/// own convention for a literal dollar sign, and a `$` with no matching close is
|
||||
/// left alone too, rather than swallowing the rest of the field.
|
||||
fn rewrite_latex_math(source: &str) -> String {
|
||||
let chars: Vec<(usize, char)> = source.char_indices().collect();
|
||||
let mut out = String::with_capacity(source.len());
|
||||
let mut i = 0;
|
||||
while i < chars.len() {
|
||||
let (_, c) = chars[i];
|
||||
if c == '\\' && i + 1 < chars.len() {
|
||||
out.push('\\');
|
||||
out.push(chars[i + 1].1);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if c != '$' {
|
||||
out.push(c);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let mut j = i + 1;
|
||||
let close = loop {
|
||||
if j >= chars.len() {
|
||||
break None;
|
||||
}
|
||||
match chars[j].1 {
|
||||
'\\' => j += 2,
|
||||
'$' => break Some(j),
|
||||
_ => j += 1,
|
||||
}
|
||||
};
|
||||
match close {
|
||||
Some(close_idx) => {
|
||||
let start = chars[i + 1].0;
|
||||
let end = chars[close_idx].0;
|
||||
out.push_str("#mi(");
|
||||
out.push_str(&Value::str(&source[start..end]).to_typst(0));
|
||||
out.push(')');
|
||||
i = close_idx + 1;
|
||||
}
|
||||
None => {
|
||||
out.push('$');
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn markup_value(source: &str, content: bool) -> Value {
|
||||
let source = rewrite_latex_math(source);
|
||||
if content {
|
||||
Value::content(source)
|
||||
} else {
|
||||
@@ -1221,3 +1274,17 @@ mod tests {
|
||||
assert!(!balanced("closing ] first"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latex_math_becomes_a_mitex_call() {
|
||||
assert_eq!(
|
||||
rewrite_latex_math("angle $\\phi$ (phi)"),
|
||||
"angle #mi(\"\\\\phi\") (phi)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaped_and_unmatched_dollar_signs_are_left_alone() {
|
||||
assert_eq!(rewrite_latex_math("costs \\$5 total"), "costs \\$5 total");
|
||||
assert_eq!(rewrite_latex_math("just $5"), "just $5");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// templates/typst.yaml under `extra` rather than editing the geometry here, and
|
||||
// check one printed page against your scanner before running a class through it.
|
||||
|
||||
#import "@preview/mitex:0.2.7": mi
|
||||
|
||||
// coursebank:begin data
|
||||
#let cb-data = (
|
||||
course: (code: "COURSE 101", title: "Sample Course", term: "2026s"),
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
// copy — export the `key` variant instead, or the day you forget an `if` is the
|
||||
// day the class gets the answers.
|
||||
|
||||
#import "@preview/mitex:0.2.7": mi
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Metadata
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -52,12 +54,14 @@
|
||||
// how a course changes the look without editing this file at all.
|
||||
#let extra = cb-meta.at("extra", default: (:))
|
||||
#let accent = rgb(extra.at("accent", default: "#1f4e79"))
|
||||
#let body-font = extra.at("font", default: "Libertinus Serif")
|
||||
#let body-font = extra.at("font", default: "Roboto")
|
||||
#let body-size = eval(extra.at("font-size", default: "11pt"))
|
||||
#let paper = extra.at("paper", default: "us-letter")
|
||||
#let show-name-block = extra.at("name-block", default: true)
|
||||
#let show-points = extra.at("show-points", default: true)
|
||||
#let page-per-item = extra.at("page-per-item", default: false)
|
||||
#let show-bubbles = extra.at("show-bubbles", default: true)
|
||||
#let bubble-radius = eval(extra.at("bubble-radius", default: "0.42em"))
|
||||
|
||||
#let form-note = if cb-meta.form.at("count", default: 1) > 1 {
|
||||
" · Form " + cb-meta.form.id
|
||||
@@ -120,6 +124,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// An unfilled bubble a student marks by hand. `bubble-radius` is the same knob
|
||||
// the standalone answer sheet reads, so the two stay visually consistent if you
|
||||
// ever generate both.
|
||||
#let bubble() = circle(radius: bubble-radius, stroke: 0.5pt)
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// The renderer
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -154,11 +163,19 @@
|
||||
|
||||
block(inset: (left: 1.2em))[
|
||||
#for opt in q.at("options", default: ()) {
|
||||
grid(
|
||||
columns: (1.4em, 1fr),
|
||||
gutter: 0.2em,
|
||||
[#(opt.letter + ".")], [#markup(opt.text)],
|
||||
)
|
||||
if show-bubbles {
|
||||
grid(
|
||||
columns: (1.6em, 1.4em, 1fr),
|
||||
gutter: 0.2em,
|
||||
align(horizon)[#bubble()], [#(opt.letter + ".")], [#markup(opt.text)],
|
||||
)
|
||||
} else {
|
||||
grid(
|
||||
columns: (1.4em, 1fr),
|
||||
gutter: 0.2em,
|
||||
[#(opt.letter + ".")], [#markup(opt.text)],
|
||||
)
|
||||
}
|
||||
v(0.15em)
|
||||
}
|
||||
]
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
// paper's config, and it is why these are two templates rather than one with a
|
||||
// flag.
|
||||
|
||||
#import "@preview/mitex:0.2.7": mi
|
||||
|
||||
// coursebank:begin data
|
||||
#let cb-data = (
|
||||
course: (code: "COURSE 101", title: "Sample Course", term: "2026s"),
|
||||
|
||||
Reference in New Issue
Block a user