use moss_core::ast::{parse_with_config, render_document, DefaultHooks, ParseConfig};
fn render(markdown: &str, math: bool) -> String {
let config = ParseConfig {
math,
..Default::default()
};
let doc = parse_with_config(markdown, &config);
render_document(&doc, &DefaultHooks::new())
}
#[test]
fn math_is_never_silently_dropped() {
let html = render("Energy $E = mc^2$.", true);
assert!(
!html.contains("<p>Energy .</p>"),
"math was SILENTLY DELETED (the ENABLE_MATH-without-arms bug): {html}"
);
assert!(
html.contains("E = mc^2"),
"the LaTeX source must survive to the output: {html}"
);
assert!(html.contains("Energy "), "prose lost: {html}");
}
#[test]
fn inline_math_renders_as_moss_math_code_span() {
let html = render("$E = mc^2$", true);
assert!(
html.contains(r#"<code class="moss-math" data-moss-math="inline">$E = mc^2$</code>"#),
"unexpected inline math markup: {html}"
);
}
#[test]
fn a_false_positive_math_span_loses_no_characters() {
let html = render("一个$5,两个$10", true);
assert!(
html.contains("一个") && html.contains("$5,两个$") && html.contains("10"),
"currency prose lost characters to the math fallback: {html}"
);
let text: String = html
.replace(r#"<code class="moss-math" data-moss-math="inline">"#, "")
.replace("</code>", "");
assert!(
text.contains("一个$5,两个$10"),
"reader-visible text must equal the author's source: {text}"
);
}
#[test]
fn inline_math_is_inert_when_math_is_off() {
let html = render("$E = mc^2$", false);
assert!(
!html.contains("moss-math"),
"math:false must not parse math: {html}"
);
assert!(
html.contains("$E = mc^2$"),
"with math off the source must pass through literally: {html}"
);
}
#[test]
fn math_defaults_to_off() {
assert!(!ParseConfig::default().math);
let doc = moss_core::ast::parse("$E = mc^2$");
let html = render_document(&doc, &DefaultHooks::new());
assert!(
html.contains("$E = mc^2$"),
"default parse() changed: {html}"
);
}
#[test]
fn display_math_renders_with_display_marker() {
let html = render("$$ x^2 $$", true);
assert!(
html.contains(r#"data-moss-math="display""#),
"display math must be marked as display: {html}"
);
assert!(html.contains("x^2"), "display TeX lost: {html}");
}
#[test]
fn display_math_block_survives_on_its_own_lines() {
let html = render("Before\n\n$$\n\\frac{a}{b}\n$$\n\nAfter", true);
assert!(html.contains(r#"data-moss-math="display""#), "{html}");
assert!(html.contains(r"\frac{a}{b}"), "{html}");
assert!(html.contains("Before"), "{html}");
assert!(html.contains("After"), "{html}");
}
#[test]
fn math_source_is_html_escaped() {
let html = render("$a < b & c$", true);
assert!(
html.contains("a < b & c"),
"TeX must be HTML-escaped: {html}"
);
let span = html
.split(r#"data-moss-math="inline">"#)
.nth(1)
.and_then(|s| s.split("</code>").next())
.unwrap_or_else(|| panic!("no math span in {html}"));
assert_eq!(span, "$a < b & c$");
assert!(!span.contains('<'), "raw `<` in math span: {span:?}");
assert!(
span.replace("<", "")
.replace("&", "")
.find('&')
.is_none(),
"unescaped `&` in math span: {span:?}"
);
}
#[test]
fn math_cannot_inject_markup() {
let html = render(r#"$</code><script>alert(1)</script>$"#, true);
assert!(
!html.contains("<script>"),
"math span allowed script injection: {html}"
);
}
#[test]
fn math_survives_inside_list_items() {
let html = render("- energy $E = mc^2$ here\n- plain", true);
assert!(
html.contains("E = mc^2"),
"math dropped inside a list item — parse_inline_event whitelist is \
missing the math events: {html}"
);
}
#[test]
fn math_survives_inside_a_table_cell() {
let html = render("| a | b |\n|---|---|\n| $x^2$ | y |", true);
assert!(
html.contains("x^2"),
"math dropped inside a table cell: {html}"
);
}
mod shortcode_bodies_inherit_the_callers_config {
use moss_core::ast::parser::{parse_with_config, ParseConfig};
fn math_on() -> ParseConfig {
ParseConfig { math: true, ..Default::default() }
}
fn rendered(md: &str, config: &ParseConfig) -> String {
let doc = parse_with_config(md, config);
moss_core::ast::render::render_document(&doc, &moss_core::ast::hooks::DefaultHooks::new())
}
#[test]
fn hero_overlay_typesets_math_like_surrounding_prose() {
let html = rendered(":::hero\ntext $E=mc^2$ end\n:::\n", &math_on());
assert!(
html.contains(r#"<code class="moss-math" data-moss-math="inline">$E=mc^2$</code>"#),
"hero overlay kept literal $…$ while prose became math: {html}"
);
}
#[test]
fn grid_cell_typesets_math_like_surrounding_prose() {
let html = rendered(":::grid\nEnergy is $E=mc^2$ here\n|\nsecond cell\n:::\n", &math_on());
assert!(
html.contains(r#"data-moss-math="inline">$E=mc^2$</code>"#),
"grid cell kept literal $…$: {html}"
);
}
#[test]
fn math_off_leaves_shortcode_bodies_literal() {
let html = rendered(":::hero\ntext $E=mc^2$ end\n:::\n", &ParseConfig::default());
assert!(html.contains("$E=mc^2$"), "math=off must not typeset: {html}");
assert!(!html.contains("moss-math"));
}
#[test]
fn hero_overlay_text_keeps_math_for_the_description_chain() {
let mut doc = parse_with_config(
":::hero\nEnergy is $E=mc^2$ exactly.\n:::\n",
&math_on(),
);
let extraction = moss_core::ast::extract_hero::extract_hero(
&mut doc,
&moss_core::ast::hooks::DefaultHooks::new(),
)
.expect("hero must be extracted");
let text = extraction.overlay_text.expect("hero must yield overlay text");
assert!(text.contains("$E=mc^2$"), "meta description lost the equation: {text:?}");
}
}
mod plain_text_collectors_keep_math {
use moss_core::ast::node::{Block, Inline};
use moss_core::ast::parser::{parse_with_config, ParseConfig};
fn math_on() -> ParseConfig {
ParseConfig { math: true, ..Default::default() }
}
#[test]
fn image_alt_and_caption_keep_the_equation() {
let doc = parse_with_config("\n", &math_on());
let Block::Figure { image, caption, .. } = &doc.blocks[0] else {
panic!("expected a figure, got {:?}", doc.blocks[0]);
};
let Inline::Image { alt, .. } = image else {
panic!("expected an image");
};
assert_eq!(alt, "before $E=mc^2$ after");
assert!(!alt.contains('<'), "alt must stay plain text, got {alt:?}");
let caption = caption.as_ref().expect("implicit figure must have a caption");
assert!(
caption
.iter()
.any(|i| matches!(i, Inline::Other(html) if html.contains("data-moss-math"))),
"caption must carry the typed math node, got {caption:?}"
);
assert!(
!caption
.iter()
.any(|i| matches!(i, Inline::Text(t) if t.contains("$E=mc^2$"))),
"caption must not carry the flattened math source, got {caption:?}"
);
}
#[test]
fn callout_title_is_not_truncated_at_the_first_dollar() {
let doc = parse_with_config("> [!note] Energy $E=mc^2$ explained\n> body\n", &math_on());
let Block::Callout { title, children, .. } = &doc.blocks[0] else {
panic!("expected a callout, got {:?}", doc.blocks[0]);
};
assert_eq!(title.as_deref(), Some("Energy $E=mc^2$ explained"));
let body = format!("{children:?}");
assert!(!body.contains("explained"), "title tail leaked into body: {body}");
}
#[test]
fn heading_renders_math_but_slugs_the_source() {
let doc = parse_with_config("# Euler $e^{i\\pi}=-1$ identity\n", &math_on());
let Block::Heading { id, children, .. } = &doc.blocks[0] else {
panic!("expected a heading");
};
assert_eq!(id.as_deref(), Some("euler-$e{ipi}=-1$-identity"));
let off = parse_with_config("# Euler $e^{i\\pi}=-1$ identity\n", &ParseConfig::default());
let Block::Heading { id: off_id, .. } = &off.blocks[0] else { panic!() };
assert_eq!(id, off_id);
assert!(children.iter().any(|c| matches!(c, Inline::Other(h) if h.contains("moss-math"))));
}
}
#[test]
fn moss_math_contract_example_matches_a_real_render() {
let entry = moss_core::contract::components::COMPONENTS
.iter()
.find(|c| c.class == "moss-math")
.expect("the .moss-math component must be declared in COMPONENTS");
let html = render(entry.example_markdown, true);
assert!(
html.contains(entry.example_html),
"the declared example_html is not what moss emits for the declared \
example_markdown.\n markdown: {:?}\n declared: {}\n actual: {}",
entry.example_markdown,
entry.example_html,
html.trim()
);
}