use crate::{error::Result, formula::recognizer::FormulaRecognition};
pub const TRUNCATED_MARKER: &str = "<!-- formula truncated: no EOS token -->";
fn normalize_display_math(latex: &str) -> String {
let normalized = latex.replace("\r\n", "\n").replace('\r', "\n");
let lines: Vec<&str> = normalized
.lines()
.filter(|line| !line.trim().is_empty())
.map(str::trim_end)
.collect();
lines.join("\n").trim().replace("$$", "\\$\\$")
}
pub fn to_markdown_block(latex: &str, truncated: bool) -> String {
let latex = normalize_display_math(latex);
if latex.is_empty() {
return String::new();
}
let block = format!("$$\n{latex}\n$$");
if truncated {
format!("{block}\n\n{TRUNCATED_MARKER}")
} else {
block
}
}
pub fn order_formula_results(
results: Vec<FormulaRecognition>,
order: &[usize],
) -> Result<Vec<FormulaRecognition>> {
if order.len() != results.len() {
return Err(crate::error::RapidOcrError::InvalidInput(format!(
"formula result order length mismatch: results={}, order={}",
results.len(),
order.len()
)));
}
let mut seen = vec![false; results.len()];
let mut ordered = Vec::with_capacity(results.len());
for &index in order {
if index >= results.len() || seen[index] {
return Err(crate::error::RapidOcrError::InvalidInput(format!(
"formula result order contains invalid index {index}"
)));
}
seen[index] = true;
ordered.push(results[index].clone());
}
Ok(ordered)
}
#[cfg(test)]
mod tests {
use super::*;
fn result_with(latex: &str, truncated: bool) -> FormulaRecognition {
FormulaRecognition {
latex: latex.to_string(),
token_ids: vec![0, 82, 2],
eos_index: if truncated { None } else { Some(2) },
truncated,
model_id: "test-model".to_string(),
elapsed_ms: 1.5,
batch_size: 1,
}
}
fn result() -> FormulaRecognition {
result_with("x < y & z", false)
}
#[test]
fn markdown_uses_display_math_and_hides_tokens() {
let markdown = to_markdown_block("x < y & z", false);
assert_eq!(markdown, "$$\nx < y & z\n$$");
assert!(!markdown.contains("82"));
}
#[test]
fn markdown_escapes_inner_display_math_delimiters() {
let markdown = to_markdown_block("a $$ b", false);
assert_eq!(markdown, "$$\na \\$\\$ b\n$$");
let delimiters = markdown.matches("$$").count();
assert_eq!(delimiters, 2, "markdown: {markdown}");
}
#[test]
fn markdown_keeps_fences_as_literal_math_content() {
let markdown = to_markdown_block("```x```", false);
assert_eq!(markdown, "$$\n```x```\n$$");
assert!(!markdown.contains("````"), "markdown: {markdown}");
}
#[test]
fn markdown_normalizes_newlines_and_drops_blank_lines() {
let markdown = to_markdown_block("a\r\n b\r\n\r\n\r\nc", false);
assert_eq!(markdown, "$$\na\n b\nc\n$$");
}
#[test]
fn markdown_for_empty_latex_emits_nothing() {
assert_eq!(to_markdown_block("", false), "");
assert_eq!(to_markdown_block(" \n \n", false), "");
}
#[test]
fn markdown_marks_truncated_results_with_invisible_comment() {
let markdown = to_markdown_block("\\frac{1}{", true);
assert!(markdown.starts_with("$$\n\\frac{1}{\n$$"));
assert!(markdown.ends_with(TRUNCATED_MARKER));
assert_eq!(
markdown.matches(TRUNCATED_MARKER).count(),
1,
"markdown: {markdown}"
);
let complete = to_markdown_block("\\frac{1}{2}", false);
assert!(!complete.contains(TRUNCATED_MARKER));
}
#[test]
fn markdown_keeps_markdown_special_characters_verbatim() {
let markdown = to_markdown_block("*a* _b_ [c](d) #e", false);
assert_eq!(markdown, "$$\n*a* _b_ [c](d) #e\n$$");
}
#[test]
fn order_results_rejects_invalid_orders() {
let results = vec![result(), result(), result()];
let error = order_formula_results(results.clone(), &[0, 0, 2])
.expect_err("duplicate index must fail");
assert!(error.to_string().contains("invalid index"));
let error =
order_formula_results(results.clone(), &[0, 1]).expect_err("length mismatch must fail");
assert!(error.to_string().contains("length mismatch"));
let ordered = order_formula_results(results, &[2, 0, 1]).expect("valid order");
assert_eq!(ordered.len(), 3);
}
}