chore: import upstream snapshot with attribution
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,515 @@
|
||||
use jcode_render_core::{
|
||||
BlockKind, StyleRole, normalize_latex_math, parse_markdown, render_display_latex,
|
||||
render_inline_latex,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
fn assert_visible_and_deterministic(source: &str) {
|
||||
let inline = render_inline_latex(source);
|
||||
let display = render_display_latex(source);
|
||||
assert_eq!(inline, render_inline_latex(source), "inline: {source:?}");
|
||||
assert_eq!(display, render_display_latex(source), "display: {source:?}");
|
||||
if !source.trim().is_empty() {
|
||||
assert!(
|
||||
!inline.trim().is_empty(),
|
||||
"inline content vanished: {source:?}"
|
||||
);
|
||||
assert!(
|
||||
display.iter().any(|line| !line.trim().is_empty()),
|
||||
"display content vanished: {source:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_the_supported_symbol_vocabulary() {
|
||||
let cases = [
|
||||
(r"\alpha \beta \gamma \delta \epsilon \varepsilon", "αβγδεε"),
|
||||
(
|
||||
r"\theta \vartheta \lambda \mu \pi \varpi \phi \varphi \omega",
|
||||
"θϑλμπϖφϕω",
|
||||
),
|
||||
(
|
||||
r"\Gamma \Delta \Theta \Lambda \Xi \Pi \Sigma \Phi \Psi \Omega",
|
||||
"ΓΔΘΛΞΠΣΦΨΩ",
|
||||
),
|
||||
(
|
||||
r"\sum \prod \coprod \int \iint \iiint \oint \partial \nabla \infty",
|
||||
"∑∏∐∫∬∭∮∂∇∞",
|
||||
),
|
||||
(r"\times \div \cdot \circ \pm \mp \ast \star", "×÷·∘±∓∗⋆"),
|
||||
(
|
||||
r"\le \leq \ge \geq \ne \neq \approx \sim \simeq \equiv \propto",
|
||||
"≤≤≥≥≠≠≈∼≃≡∝",
|
||||
),
|
||||
(
|
||||
r"\in \notin \ni \subset \supset \subseteq \supseteq \cup \cap \setminus",
|
||||
"∈∉∋⊂⊃⊆⊇∪∩∖",
|
||||
),
|
||||
(
|
||||
r"\forall \exists \nexists \neg \land \lor \oplus \otimes \vdash \models",
|
||||
"∀∃∄¬∧∨⊕⊗⊢⊨",
|
||||
),
|
||||
(
|
||||
r"\to \leftarrow \leftrightarrow \Rightarrow \Leftarrow \Leftrightarrow \mapsto \uparrow \downarrow",
|
||||
"→←↔⇒⇐⇔↦↑↓",
|
||||
),
|
||||
(
|
||||
r"\ldots \cdots \vdots \ddots \angle \degree \prime \perp \parallel \mid",
|
||||
"…⋯⋮⋱∠°′⊥∥∣",
|
||||
),
|
||||
(
|
||||
r"\langle x \rangle \lceil x \rceil \lfloor x \rfloor",
|
||||
"⟨x ⟩⌈x ⌉⌊x ⌋",
|
||||
),
|
||||
(r"\emptyset \varnothing \ell \hbar", "∅∅ℓℏ"),
|
||||
];
|
||||
for (source, expected) in cases {
|
||||
assert_eq!(render_inline_latex(source), expected, "{source}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_all_supported_script_characters_and_falls_back_for_others() {
|
||||
assert_eq!(
|
||||
render_inline_latex("x^{0123456789+-=()ni}"),
|
||||
"x⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ⁿⁱ"
|
||||
);
|
||||
assert_eq!(
|
||||
render_inline_latex("x_{0123456789+-=()aehijklmnoprstuvx}"),
|
||||
"x₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ"
|
||||
);
|
||||
assert_eq!(render_inline_latex("x^{ab}_{qy}"), "x_(qy)^(ab)");
|
||||
assert_eq!(render_inline_latex("x_1^2_3^4"), "x₁₃²⁴");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formatting_commands_preserve_content_and_spacing() {
|
||||
for command in [
|
||||
"mathbf",
|
||||
"mathrm",
|
||||
"mathit",
|
||||
"mathsf",
|
||||
"mathtt",
|
||||
"mathcal",
|
||||
"mathbb",
|
||||
"boldsymbol",
|
||||
"displaystyle",
|
||||
"scriptstyle",
|
||||
] {
|
||||
assert_eq!(
|
||||
render_inline_latex(&format!(r"\{command}{{x_2+y}}")),
|
||||
"x₂+y",
|
||||
"{command}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
render_inline_latex(r"a\,b\;c\:d\quad e\qquad f\!g"),
|
||||
"a b c d e fg"
|
||||
);
|
||||
assert_eq!(
|
||||
render_inline_latex(r"\text{rate = 5 percent}"),
|
||||
"rate = 5 percent"
|
||||
);
|
||||
assert_eq!(
|
||||
render_inline_latex(r"\operatorname{arg max}_x f(x)"),
|
||||
"arg maxₓ f(x)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accents_lines_and_delimiters_are_stable_unicode() {
|
||||
assert_eq!(
|
||||
render_inline_latex(r"\hat{x}\bar{y}\vec{v}\dot{x}\ddot{y}\tilde{z}"),
|
||||
"x̂ȳv⃗ẋÿz̃"
|
||||
);
|
||||
assert_eq!(render_inline_latex(r"\overline{ab}"), "a̅b̅");
|
||||
assert_eq!(render_inline_latex(r"\underline{ab}"), "a̲b̲");
|
||||
assert_eq!(render_inline_latex(r"\left( x \right)"), "( x )");
|
||||
assert_eq!(render_inline_latex(r"\left\{ x \right\}"), "{ x }");
|
||||
assert_eq!(render_inline_latex(r"\bigl\langle x \bigr\rangle"), "⟨ x ⟩");
|
||||
assert_eq!(render_inline_latex(r"\left. x \right|"), " x |");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fraction_root_and_script_layouts_have_consistent_geometry() {
|
||||
let cases = [
|
||||
r"\frac{a}{b}",
|
||||
r"\frac{x+1}{y-z}",
|
||||
r"\frac{\frac{a}{b}}{\sqrt{x}}",
|
||||
r"x^{a+b}_{i-j}",
|
||||
r"\sqrt[3]{\frac{x}{y}}",
|
||||
r"A+\frac{界}{\alpha}+B",
|
||||
];
|
||||
for source in cases {
|
||||
let lines = render_display_latex(source);
|
||||
assert!(!lines.is_empty(), "{source}");
|
||||
let width = lines.iter().map(|line| line.width()).max().unwrap();
|
||||
assert!(width > 0, "{source}: {lines:?}");
|
||||
assert!(
|
||||
lines.iter().all(|line| line.width() <= width),
|
||||
"{source}: {lines:?}"
|
||||
);
|
||||
assert!(
|
||||
lines.iter().all(|line| !line.contains('\t')),
|
||||
"{source}: {lines:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_matrix_environment_has_the_expected_delimiter_family() {
|
||||
let cases = [
|
||||
("matrix", "", ""),
|
||||
("smallmatrix", "", ""),
|
||||
("array", "", ""),
|
||||
("pmatrix", "⎛", "⎞"),
|
||||
("bmatrix", "⎡", "⎤"),
|
||||
("Bmatrix", "⎧", "⎫"),
|
||||
("vmatrix", "│", "│"),
|
||||
("Vmatrix", "‖", "‖"),
|
||||
("cases", "⎧", ""),
|
||||
];
|
||||
for (environment, left, right) in cases {
|
||||
let source = if environment == "array" {
|
||||
r"\begin{array}{cc}a & bb \\ ccc & d\end{array}".to_string()
|
||||
} else {
|
||||
format!(r"\begin{{{environment}}}a & bb \\ ccc & d\end{{{environment}}}")
|
||||
};
|
||||
let lines = render_display_latex(&source);
|
||||
assert_eq!(lines.len(), 2, "{environment}: {lines:?}");
|
||||
if !left.is_empty() {
|
||||
assert!(lines[0].starts_with(left), "{environment}: {lines:?}");
|
||||
}
|
||||
if !right.is_empty() {
|
||||
assert!(lines[0].ends_with(right), "{environment}: {lines:?}");
|
||||
}
|
||||
assert_eq!(
|
||||
lines[0].width(),
|
||||
lines[1].width(),
|
||||
"{environment}: {lines:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matrices_handle_ragged_rows_nested_groups_and_row_spacing() {
|
||||
let ragged = render_display_latex(r"\begin{bmatrix}a & bb & ccc \\ d \\ e & ff\end{bmatrix}");
|
||||
assert_eq!(ragged.len(), 3);
|
||||
assert!(
|
||||
ragged.iter().all(|line| line.width() == ragged[0].width()),
|
||||
"{ragged:?}"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
render_inline_latex(r"\begin{matrix}{a & b} & \frac{c&d}{e} \\ x & y\end{matrix}"),
|
||||
"a & b, (c&d)⁄e; x, y"
|
||||
);
|
||||
assert_eq!(
|
||||
render_inline_latex(r"\begin{matrix}a\\[12pt]b\\[-2pt]c\\\end{matrix}"),
|
||||
"a; b; c"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_display_environments_render_their_body() {
|
||||
for environment in ["equation", "equation*", "displaymath"] {
|
||||
let source = format!(r"\begin{{{environment}}}\frac{{x}}{{y}}\end{{{environment}}}");
|
||||
let lines = render_display_latex(&source);
|
||||
assert!(
|
||||
lines.iter().any(|line| line.contains('─')),
|
||||
"{source}: {lines:?}"
|
||||
);
|
||||
assert!(
|
||||
!lines.iter().any(|line| line.contains("begin")),
|
||||
"{source}: {lines:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_commands_and_environments_remain_debuggable() {
|
||||
let cases = [
|
||||
(r"\unknown", r"\unknown"),
|
||||
(r"\unknown value", r"\unknown value"),
|
||||
(r"\unknown{value}", r"\unknownvalue"),
|
||||
(
|
||||
r"\begin{mystery}x+y\end{mystery}",
|
||||
r"\begin{mystery}x+y\end{mystery}",
|
||||
),
|
||||
(r"\end{orphan}", r"\endorphan"),
|
||||
];
|
||||
for (source, expected) in cases {
|
||||
assert_eq!(render_inline_latex(source), expected, "{source}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_constructs_never_panic_and_keep_diagnostic_content() {
|
||||
let cases = [
|
||||
"{",
|
||||
"}",
|
||||
"[",
|
||||
"]",
|
||||
"^",
|
||||
"_",
|
||||
"\\",
|
||||
r"\frac",
|
||||
r"\frac{}",
|
||||
r"\frac{}{}",
|
||||
r"\sqrt",
|
||||
r"\sqrt[",
|
||||
r"\sqrt[]{}",
|
||||
r"\text",
|
||||
r"\left",
|
||||
r"\right",
|
||||
r"\begin",
|
||||
r"\begin{}",
|
||||
r"\begin{matrix}",
|
||||
r"\begin{matrix}a&b\end{pmatrix}",
|
||||
r"x_{",
|
||||
r"x^{}}",
|
||||
r"x^^__",
|
||||
"😀_{界",
|
||||
"\0\u{1b}[31m",
|
||||
];
|
||||
for source in cases {
|
||||
assert_visible_and_deterministic(source);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_latex_grammar_corpus_is_total_and_deterministic() {
|
||||
let atoms = ["x", "7", "α", "界", "😀", r"\beta", r"\unknown"];
|
||||
let wrappers = [
|
||||
(r"{", "}"),
|
||||
(r"\frac{", "}{2}"),
|
||||
(r"\sqrt{", "}"),
|
||||
(r"\sqrt[3]{", "}"),
|
||||
(r"\mathbf{", "}"),
|
||||
(r"\left(", r"\right)"),
|
||||
(r"\begin{bmatrix}", r" & y \\ z & w\end{bmatrix}"),
|
||||
];
|
||||
let scripts = ["", "_1", "^2", "_{i+j}", "^{n-1}", "_i^2"];
|
||||
for atom in atoms {
|
||||
for (open, close) in wrappers {
|
||||
for script in scripts {
|
||||
let source = format!("{open}{atom}{close}{script}");
|
||||
assert_visible_and_deterministic(&source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_flat_inputs_and_deep_commands_remain_bounded() {
|
||||
let flat = format!("{}{}", r"\alpha+".repeat(20_000), "x");
|
||||
let inline = render_inline_latex(&flat);
|
||||
assert!(inline.ends_with('x'));
|
||||
assert!(inline.len() < flat.len());
|
||||
|
||||
let mut nested = "x".to_string();
|
||||
for _ in 0..2_000 {
|
||||
nested = format!(r"\sqrt{{{nested}}}");
|
||||
}
|
||||
std::thread::Builder::new()
|
||||
.stack_size(256 * 1024)
|
||||
.spawn(move || assert_visible_and_deterministic(&nested))
|
||||
.expect("spawn bounded stack test")
|
||||
.join()
|
||||
.expect("deep command rendering must not panic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_every_supported_math_fence_spelling() {
|
||||
for info in [
|
||||
"math",
|
||||
"MATH",
|
||||
"latex",
|
||||
"LaTeX",
|
||||
"tex",
|
||||
"TEX",
|
||||
"katex",
|
||||
"KaTeX",
|
||||
"{.math}",
|
||||
"{latex}",
|
||||
"math title=demo",
|
||||
] {
|
||||
let source = format!("```{info}\n\\alpha_2\n```");
|
||||
assert_eq!(normalize_latex_math(&source), "$$\n\\alpha_2\n$$", "{info}");
|
||||
}
|
||||
assert_eq!(
|
||||
normalize_latex_math(" ~~~~latex\n\\[x^2\\]\n ~~~~~"),
|
||||
" $$\nx^2\n $$"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalization_protects_all_literal_code_forms() {
|
||||
let cases = [
|
||||
r"`\(x\)`",
|
||||
r"``code ` and \[x\]``",
|
||||
"```rust\n\\(x\\)\n```",
|
||||
"~~~text\n\\begin{matrix}x\\end{matrix}\n~~~",
|
||||
" \\[indented code\\]",
|
||||
"\t\\(tab-indented code\\)",
|
||||
];
|
||||
for source in cases {
|
||||
assert_eq!(normalize_latex_math(source), source, "{source:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fence_like_content_does_not_end_a_generic_code_fence() {
|
||||
for source in [
|
||||
"```text\n```not a closing fence\n\\(literal\\)\n```",
|
||||
"~~~~text\n~~~~still content\n\\[literal\\]\n~~~~",
|
||||
"````rust\n``` shorter fence\n\\begin{matrix}x\\end{matrix}\n````",
|
||||
] {
|
||||
assert_eq!(normalize_latex_math(source), source, "{source:?}");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
normalize_latex_math("```text\n\\(literal\\)\n```\n\n\\(real math\\)"),
|
||||
"```text\n\\(literal\\)\n```\n\n$real math$"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_delimiters_only_when_balanced_and_unescaped() {
|
||||
let cases = [
|
||||
(r"before \(x^2\) after", r"before $x^2$ after"),
|
||||
(r"before \[x^2\] after", r"before $$x^2$$ after"),
|
||||
(r"\\(literal\\)", r"\\(literal\\)"),
|
||||
(r"\(missing", r"\(missing"),
|
||||
(r"missing\)", r"missing\)"),
|
||||
(r"\[label\](url)", r"\[label\](url)"),
|
||||
(r"$x + \(y\)$", r"$x + \(y\)$"),
|
||||
(r"$$x + \[y\]$$", r"$$x + \[y\]$$"),
|
||||
];
|
||||
for (source, expected) in cases {
|
||||
assert_eq!(normalize_latex_math(source), expected, "{source}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_all_standalone_display_environments() {
|
||||
let environments = [
|
||||
"equation",
|
||||
"equation*",
|
||||
"displaymath",
|
||||
"align",
|
||||
"align*",
|
||||
"aligned",
|
||||
"aligned*",
|
||||
"gather",
|
||||
"gather*",
|
||||
"gathered",
|
||||
"multline",
|
||||
"multline*",
|
||||
"split",
|
||||
"eqnarray",
|
||||
"eqnarray*",
|
||||
"matrix",
|
||||
"smallmatrix",
|
||||
"array",
|
||||
"pmatrix",
|
||||
"bmatrix",
|
||||
"Bmatrix",
|
||||
"vmatrix",
|
||||
"Vmatrix",
|
||||
"cases",
|
||||
"cases*",
|
||||
];
|
||||
for environment in environments {
|
||||
let source = format!(r"prefix \begin{{{environment}}}x\end{{{environment}}} suffix");
|
||||
let normalized = normalize_latex_math(&source);
|
||||
assert!(normalized.contains("$$\n"), "{environment}: {normalized:?}");
|
||||
assert!(normalized.contains("\n$$"), "{environment}: {normalized:?}");
|
||||
assert!(normalized.contains(&format!(r"\begin{{{environment}}}x\end{{{environment}}}")));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_normalization_handles_nesting_and_mismatches() {
|
||||
let nested = r"\begin{align}a\begin{align}b\end{align}c\end{align}";
|
||||
let normalized = normalize_latex_math(nested);
|
||||
assert_eq!(normalized.matches("$$").count(), 2, "{normalized}");
|
||||
assert!(normalized.contains(nested));
|
||||
|
||||
for source in [
|
||||
r"\begin{align}x",
|
||||
r"\begin{align}x\end{equation}",
|
||||
r"\begin{unknown}x\end{unknown}",
|
||||
] {
|
||||
assert_eq!(normalize_latex_math(source), source, "{source}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_pipeline_preserves_structure_and_math_roles() {
|
||||
let markdown = concat!(
|
||||
"Price: $35.00 and inline \\(x_2 + \\alpha\\).\n\n",
|
||||
"\\[\\frac{x+1}{y}\\]\n\n",
|
||||
"```rust\nlet literal = r\"\\(x\\)\";\n```\n"
|
||||
);
|
||||
let doc = parse_markdown(markdown);
|
||||
assert!(
|
||||
doc.blocks
|
||||
.iter()
|
||||
.any(|block| block.kind == BlockKind::Paragraph)
|
||||
);
|
||||
let display = doc
|
||||
.blocks
|
||||
.iter()
|
||||
.find(|block| block.kind == BlockKind::MathDisplay)
|
||||
.expect("display math block");
|
||||
assert!(
|
||||
display
|
||||
.lines
|
||||
.iter()
|
||||
.all(|line| line.spans.iter().all(|span| span.role == StyleRole::Math))
|
||||
);
|
||||
assert!(
|
||||
display
|
||||
.lines
|
||||
.iter()
|
||||
.any(|line| line.plain_text().contains('─'))
|
||||
);
|
||||
|
||||
let all_text = doc
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|block| &block.lines)
|
||||
.map(|line| line.plain_text())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(all_text.contains("$35.00"), "{all_text}");
|
||||
assert!(all_text.contains("x₂ + α"), "{all_text}");
|
||||
assert!(all_text.contains(r"\(x\)"), "{all_text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_container_matrix_has_backend_neutral_parity() {
|
||||
let containers = [
|
||||
r"Inline \(\alpha_2 + x^2\).",
|
||||
r"\[\frac{x+1}{y}\]",
|
||||
"```math\n\\frac{x+1}{y}\n```",
|
||||
"~~~latex\n\\begin{bmatrix}a & b \\\\ c & d\\end{bmatrix}\n~~~",
|
||||
r"\begin{align*}x &= 1 \\ y &= 2\end{align*}",
|
||||
];
|
||||
for markdown in containers {
|
||||
let first = parse_markdown(markdown);
|
||||
let second = parse_markdown(markdown);
|
||||
assert_eq!(first, second, "{markdown}");
|
||||
assert!(
|
||||
first
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|block| &block.lines)
|
||||
.flat_map(|line| &line.spans)
|
||||
.any(|span| span.role == StyleRole::Math),
|
||||
"{markdown}: {first:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
use jcode_render_core::{BlockKind, normalize_latex_math, parse_markdown};
|
||||
|
||||
fn math_display_count(markdown: &str) -> usize {
|
||||
parse_markdown(markdown)
|
||||
.blocks
|
||||
.iter()
|
||||
.filter(|block| block.kind == BlockKind::MathDisplay)
|
||||
.count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_the_exact_multiline_equation_response_from_the_tui() {
|
||||
let response = concat!(
|
||||
"\\[\n\\boxed{\ne^{i\\pi}+1=0\n}\n\\]\n\n",
|
||||
"\\[\n\\int_{-\\infty}^{\\infty} e^{-x^2}\\,dx=\\sqrt{\\pi}\n\\]\n\n",
|
||||
"\\[\nx=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}\n\\]\n\n",
|
||||
"\\[\n\\nabla\\cdot\\mathbf{E}=\\frac{\\rho}{\\varepsilon_0}\n\\]\n\n",
|
||||
"\\[\n\\frac{\\partial \\psi}{\\partial t}\n=\n",
|
||||
"\\alpha\\frac{\\partial^2\\psi}{\\partial x^2}\n\\]",
|
||||
);
|
||||
|
||||
let normalized = normalize_latex_math(response);
|
||||
let parsed = parse_markdown(response);
|
||||
assert_eq!(
|
||||
math_display_count(response),
|
||||
5,
|
||||
"normalized={normalized:?} parsed={parsed:#?}"
|
||||
);
|
||||
assert_eq!(normalized.matches("$$").count(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_streaming_prefix_is_deterministic_and_the_complete_response_is_math() {
|
||||
let equation = concat!(
|
||||
"Result:\n\n\\[\n",
|
||||
"\\frac{\\partial \\psi}{\\partial t}\n=\n",
|
||||
"\\alpha\\frac{\\partial^2\\psi}{\\partial x^2}\n\\]",
|
||||
);
|
||||
|
||||
for end in equation
|
||||
.char_indices()
|
||||
.map(|(index, _)| index)
|
||||
.chain(std::iter::once(equation.len()))
|
||||
{
|
||||
let prefix = &equation[..end];
|
||||
let first = parse_markdown(prefix);
|
||||
let second = parse_markdown(prefix);
|
||||
assert_eq!(
|
||||
first, second,
|
||||
"nondeterministic prefix ending at byte {end}"
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(math_display_count(equation), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_math_survives_common_markdown_containers() {
|
||||
let cases = [
|
||||
("> \\[\n> x^2\n> =\n> y^2\n> \\]", "blockquote"),
|
||||
("- \\[\n x^2\n =\n y^2\n \\]", "bullet list"),
|
||||
("1. \\[\n x^2\n =\n y^2\n \\]", "ordered list"),
|
||||
];
|
||||
|
||||
for (source, label) in cases {
|
||||
assert!(
|
||||
math_display_count(source) >= 1,
|
||||
"{label} lost display math after normalization: {:?}",
|
||||
normalize_latex_math(source)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standalone_display_math_resists_markdown_block_interruptors() {
|
||||
let bodies = [
|
||||
"x\n=\ny",
|
||||
"x\n---\ny",
|
||||
"x\n\ny",
|
||||
"x\n# not a heading\ny",
|
||||
"x\n> not a quote\ny",
|
||||
"x\n- not a list\ny",
|
||||
"x\n1. not a list\ny",
|
||||
"x\n``` not a fence\ny",
|
||||
];
|
||||
|
||||
for body in bodies {
|
||||
for source in [format!("$$\n{body}\n$$"), format!("\\[\n{body}\n\\]")] {
|
||||
assert_eq!(
|
||||
math_display_count(&source),
|
||||
1,
|
||||
"block syntax escaped display math: {source:?}; normalized={:?}",
|
||||
normalize_latex_math(&source)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crlf_and_adjacent_display_blocks_remain_balanced_and_idempotent() {
|
||||
let source = "\\[\r\nx\r\n=\r\ny\r\n\\]\r\n\r\n$$\r\na+b\r\n$$";
|
||||
let normalized = normalize_latex_math(source);
|
||||
assert_eq!(math_display_count(source), 2, "{normalized:?}");
|
||||
assert_eq!(normalize_latex_math(&normalized), normalized);
|
||||
assert_eq!(normalized.matches("$$").count(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stabilization_preserves_newlines_comments_and_is_idempotent() {
|
||||
let source = "\\[\na % this comment must still end at the newline\nb\n=\nc\n\\]";
|
||||
let normalized = normalize_latex_math(source);
|
||||
|
||||
assert_eq!(
|
||||
normalized,
|
||||
"$$\na % this comment must still end at the newline\nb\n{}=\nc\n$$"
|
||||
);
|
||||
assert_eq!(normalize_latex_math(&normalized), normalized);
|
||||
assert_eq!(math_display_count(source), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_quote_list_display_math_keeps_its_container() {
|
||||
let source = "> - \\[\n> x\n> =\n> y\n> \\]";
|
||||
let normalized = normalize_latex_math(source);
|
||||
|
||||
assert!(normalized.contains("> {}="), "{normalized:?}");
|
||||
assert_eq!(math_display_count(source), 1, "{normalized:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaped_and_literal_delimiters_never_become_math() {
|
||||
let cases = [
|
||||
r"\\[escaped\\]",
|
||||
r"`\\[inline code\\]`",
|
||||
"```text\n\\[fenced code\\]\n```",
|
||||
" \\[indented code\\]",
|
||||
r"\\[missing close",
|
||||
];
|
||||
|
||||
for source in cases {
|
||||
assert_eq!(
|
||||
math_display_count(source),
|
||||
0,
|
||||
"literal case parsed as math: {source:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let unclosed_display = "$$\nx\n=\ny";
|
||||
assert_eq!(normalize_latex_math(unclosed_display), unclosed_display);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standalone_multiline_inline_delimiters_promote_without_losing_content() {
|
||||
for source in [
|
||||
"\\(\nx\n=\ny\n\\)",
|
||||
"$\nx\n=\ny\n$",
|
||||
"> $\n> x\n> =\n> y\n> $",
|
||||
"- $\n x\n =\n y\n $",
|
||||
"$\r\nx\r\n=\r\ny\r\n$",
|
||||
] {
|
||||
let normalized = normalize_latex_math(source);
|
||||
let visible = parse_markdown(source)
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|block| &block.lines)
|
||||
.map(|line| line.plain_text())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
for expected in ["x", "=", "y"] {
|
||||
assert!(visible.contains(expected), "{source:?} => {visible:?}");
|
||||
}
|
||||
assert_eq!(
|
||||
math_display_count(source),
|
||||
1,
|
||||
"{source:?} => {normalized:?}"
|
||||
);
|
||||
assert_eq!(normalize_latex_math(&normalized), normalized);
|
||||
}
|
||||
|
||||
let unclosed = "$\nx\n=\ny";
|
||||
assert_eq!(normalize_latex_math(unclosed), unclosed);
|
||||
|
||||
let fenced = "```text\n$\nx\n$\n```";
|
||||
assert_eq!(normalize_latex_math(fenced), fenced);
|
||||
assert_eq!(math_display_count(fenced), 0);
|
||||
}
|
||||
Reference in New Issue
Block a user