Skip to main content

mdwright_math/
render.rs

1//! Delimiter rewrite helper for dollar-shaped renderer output.
2//!
3//! mdwright never typesets math itself. Downstream renderers
4//! (`KaTeX`, `MathJax`, `mkdocs-material`'s math plugin) do. The
5//! recogniser tags each math region with its source delimiter family;
6//! this module rewrites those delimiters at emit time so the resulting
7//! bytes match the shape the downstream renderer expects.
8//!
9//! The pass is a function rather than a method on [`super::MathRegion`]
10//! because canonicalisation already has the `(&MathSpan, &Range<usize>)`
11//! pair in scope.
12
13use std::borrow::Cow;
14use std::ops::Range;
15
16use super::span::MathSpan;
17/// Rewrite the source bytes of a math region to dollar delimiters.
18///
19/// `range` is the **outer** byte range (delimiters + body, in source).
20/// `span` is the scanner-tagged classification.
21///
22/// - [`MathSpan::Environment`]: borrow source unchanged — there is no
23///   dollar form of `\begin{name}…\end{name}`.
24/// - [`MathSpan::Inline`]: emit `${body}$`.
25/// - [`MathSpan::Display`]: emit `$$ {body} $$`.
26///
27/// The body is read through [`super::span::MathBody::as_str`], which
28/// already strips container prefixes (blockquote `>`, list-item
29/// continuation indentation). Hand-slicing the source and stripping
30/// the delimiters here would re-do that work and silently break for
31/// container-nested math.
32pub fn convert_for_dollar<'a>(source: &'a str, range: &Range<usize>, span: &MathSpan) -> Cow<'a, str> {
33    match span {
34        MathSpan::Environment { .. } => Cow::Borrowed(source.get(range.clone()).unwrap_or("")),
35        MathSpan::Display { body, .. } => {
36            let body = body.as_str(source);
37            Cow::Owned(format!("$$ {} $$", body.trim()))
38        }
39        MathSpan::Inline { body, .. } => {
40            let body = body.as_str(source);
41            Cow::Owned(format!("${}$", body.trim()))
42        }
43    }
44}
45
46#[cfg(test)]
47#[allow(clippy::indexing_slicing, clippy::panic)]
48mod tests {
49    use super::*;
50    use crate::env::{EnvKind, KnownEnv};
51    use crate::span::{DisplayDelim, InlineDelim, MathBody};
52
53    #[test]
54    fn dollar_rewrites_display_bracket() {
55        let source = r"\[ A = B \]";
56        let body = MathBody::new(2..9, Box::new([]));
57        let span = MathSpan::Display {
58            delim: DisplayDelim::Bracket,
59            body,
60        };
61        let out = convert_for_dollar(source, &(0..source.len()), &span);
62        assert_eq!(&*out, "$$ A = B $$");
63    }
64
65    #[test]
66    fn dollar_rewrites_inline_paren() {
67        let source = r"\(B\)";
68        let body = MathBody::new(2..3, Box::new([]));
69        let span = MathSpan::Inline {
70            delim: InlineDelim::Paren,
71            body,
72        };
73        let out = convert_for_dollar(source, &(0..source.len()), &span);
74        assert_eq!(&*out, "$B$");
75    }
76
77    #[test]
78    fn dollar_rewrites_inline_dollar_unchanged_shape() {
79        // Source already uses dollar — output is still dollar, body trimmed.
80        let source = "$x + y$";
81        let body = MathBody::new(1..6, Box::new([]));
82        let span = MathSpan::Inline {
83            delim: InlineDelim::Dollar,
84            body,
85        };
86        let out = convert_for_dollar(source, &(0..source.len()), &span);
87        assert_eq!(&*out, "$x + y$");
88    }
89
90    #[test]
91    fn dollar_passes_environment_through() {
92        let source = "\\begin{align*}\na &= b\n\\end{align*}";
93        // env-kind lookup wants the source slice; the actual kind
94        // doesn't matter for the pass-through assertion.
95        let env = EnvKind::Known(KnownEnv::AlignStar);
96        let body = MathBody::new(14..22, Box::new([]));
97        let span = MathSpan::Environment { env, body };
98        let out = convert_for_dollar(source, &(0..source.len()), &span);
99        assert!(matches!(out, Cow::Borrowed(_)));
100        assert_eq!(&*out, source);
101    }
102
103    #[test]
104    fn dollar_blockquote_nested_strips_transparent_runs() {
105        // `> \[ x \]` — the body range covers ` x `; the transparent
106        // run covers the `> ` prefix that would be doubled if we did
107        // not consult MathBody::as_str.
108        let source = "> \\[ x \\]";
109        // Body between delimiters: ` x ` at bytes 4..7.
110        let body = MathBody::new(4..7, Box::new([]));
111        let span = MathSpan::Display {
112            delim: DisplayDelim::Bracket,
113            body,
114        };
115        let out = convert_for_dollar(source, &(2..9), &span);
116        assert_eq!(&*out, "$$ x $$");
117    }
118}