use std::sync::OnceLock;
pub const MERMAID_RETRY_MAX: usize = 2;
#[derive(Debug, Clone)]
pub struct MermaidIssue {
pub block_index: usize,
pub message: String,
}
fn shared_engine() -> &'static merman_core::Engine {
static ENGINE: OnceLock<merman_core::Engine> = OnceLock::new();
ENGINE.get_or_init(merman_core::Engine::new)
}
pub fn validate_mermaid_blocks(content: &str) -> Vec<MermaidIssue> {
let mut issues = Vec::new();
for (idx, block) in extract_mermaid_blocks(content).iter().enumerate() {
if let Err(e) = shared_engine().parse_diagram_sync(block, merman_core::ParseOptions::strict())
{
issues.push(MermaidIssue {
block_index: idx,
message: e.to_string(),
});
}
}
issues
}
fn fence_is_mermaid(line: &str) -> bool {
let trimmed = line.trim_start();
let Some(rest) = trimmed.strip_prefix("```") else {
return false;
};
rest.trim_end().eq_ignore_ascii_case("mermaid")
}
fn extract_mermaid_blocks(content: &str) -> Vec<&str> {
let mut blocks = Vec::new();
let mut in_fence = false;
let mut fence_start = 0usize;
for (i, line) in content.lines().enumerate() {
let trimmed = line.trim_start();
if in_fence {
if trimmed.starts_with("```") {
let start_line = fence_start;
let end_line = i;
if end_line > start_line {
blocks.push(slice_lines(content, start_line, end_line));
}
in_fence = false;
}
} else if fence_is_mermaid(line) {
in_fence = true;
fence_start = i + 1;
}
}
if in_fence {
let start_line = fence_start;
let all_lines: Vec<&str> = content.lines().collect();
if start_line < all_lines.len() {
blocks.push(slice_lines(content, start_line, all_lines.len()));
}
}
blocks
}
fn slice_lines(content: &str, start: usize, end: usize) -> &str {
let mut offset = 0usize;
let mut line_start = 0usize;
for (i, line) in content.lines().enumerate() {
if i == start {
line_start = offset;
}
if i == end - 1 {
return &content[line_start..offset + line.len()];
}
offset += line.len() + 1; }
&content[line_start..]
}
pub fn mermaid_retry_feedback(issues: &[MermaidIssue]) -> String {
let mut out = String::from(
"你输出的 Markdown 中包含 Mermaid 语法错误的代码块,请修正后重试。\n\
错误清单(块号从 0 开始计数,按正文出现顺序):\n",
);
for issue in issues {
out.push_str(&format!("- 块 {}: {}\n", issue.block_index, issue.message));
}
out.push_str("请确保修正后的 Mermaid 语法合法,或改用普通文本描述。");
out
}
pub fn degrade_mermaid_blocks(content: &str, issues: &[MermaidIssue]) -> String {
let mut out = String::with_capacity(content.len() + 256);
let mut pending_degrade: Option<String> = None; let mut text_fence_open = false;
for (i, line) in content.lines().enumerate() {
let trimmed = line.trim_start();
if let Some(msg) = pending_degrade.take() {
out.push_str(&format!(
"<!-- code-repo-wiki: mermaid parse failed: {} -->\n```text\n{}\n",
sanitize_message(&msg),
line
));
text_fence_open = true;
continue;
}
if text_fence_open && trimmed.starts_with("```") {
out.push_str(line);
out.push('\n');
text_fence_open = false;
continue;
}
if fence_is_mermaid(line) {
let block_index = count_mermaid_fences_before(content, i);
if let Some(issue) = issues.iter().find(|i| i.block_index == block_index) {
pending_degrade = Some(issue.message.clone());
continue;
}
}
out.push_str(line);
out.push('\n');
}
if text_fence_open || pending_degrade.is_some() {
if let Some(msg) = pending_degrade {
out.push_str(&format!(
"<!-- code-repo-wiki: mermaid parse failed: {} -->\n```text\n",
sanitize_message(&msg)
));
}
out.push_str("```\n");
}
out
}
fn count_mermaid_fences_before(content: &str, fence_line: usize) -> usize {
content
.lines()
.take(fence_line)
.filter(|l| fence_is_mermaid(l))
.count()
}
fn sanitize_message(msg: &str) -> String {
msg.replace("-->", "-→")
.chars()
.map(|c| if c == '\n' || c == '\r' { ' ' } else { c })
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_ok_diagram() {
let content = "## 图\n\n```mermaid\nflowchart LR\nA[Start] --> B[End]\n```\n";
assert!(validate_mermaid_blocks(content).is_empty());
}
#[test]
fn test_validate_bad_diagram() {
let content = "```mermaid\nflowchart LR\nA[hello world\nB --> C\n```\n";
let issues = validate_mermaid_blocks(content);
assert_eq!(issues.len(), 1, "应识别出 1 个坏块");
assert!(issues[0].message.contains("Unterminated"), "错误消息应可读: {}", issues[0].message);
}
#[test]
fn test_validate_no_fence() {
let content = "纯文本,没有代码块\n";
assert!(validate_mermaid_blocks(content).is_empty());
}
#[test]
fn test_validate_multiple_blocks() {
let content = "```mermaid\nflowchart LR\nA --> B\n```\n\n```mermaid\nflowchart TD\nC[unterminated\n```\n";
let issues = validate_mermaid_blocks(content);
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].block_index, 1);
}
#[test]
fn test_validate_unclosed_fence_reports_error() {
let content = "```mermaid\nflowchart LR\nA[hello world\n";
let issues = validate_mermaid_blocks(content);
assert_eq!(issues.len(), 1, "未闭合围栏应作为坏块报出");
assert!(issues[0].message.contains("Unterminated"), "错误消息应可读: {}", issues[0].message);
}
#[test]
fn test_degrade_replaces_only_bad_block() {
let content = "```mermaid\nflowchart LR\nA[bad\n```\n\n```mermaid\nflowchart LR\nA[OK] --> B\n```\n";
let issues = validate_mermaid_blocks(content);
let degraded = degrade_mermaid_blocks(content, &issues);
assert!(degraded.contains("```text"), "坏块应降级为 text fence");
assert!(degraded.contains("code-repo-wiki: mermaid parse failed"), "应含降级注释");
assert!(degraded.contains("Unterminated"), "注释应含错误消息");
assert!(degraded.contains("```mermaid\nflowchart LR\nA[OK] --> B\n```"), "好块应保留");
assert!(degraded.contains("A[bad"), "坏块内容应保留");
}
#[test]
fn test_degrade_no_issues_noop() {
let content = "```mermaid\nflowchart LR\nA --> B\n```\n";
let degraded = degrade_mermaid_blocks(content, &[]);
assert_eq!(degraded, content, "无坏块时输出应与输入一致");
}
#[test]
fn test_retry_feedback_lists_blocks() {
let issues = vec![MermaidIssue {
block_index: 0,
message: "Unterminated node label (missing `]`)".into(),
}];
let fb = mermaid_retry_feedback(&issues);
assert!(fb.contains("块 0"), "反馈应含块号");
assert!(fb.contains("Unterminated"), "反馈应含错误消息");
}
#[test]
fn test_sanitize_message_single_line() {
assert_eq!(sanitize_message("a\nb\r\nc"), "a b c");
}
}
#[test]
fn test_degrade_unclosed_fence_closes_fence() {
let content = "```mermaid\nflowchart LR\nA[hello world\n";
let issues = validate_mermaid_blocks(content);
assert_eq!(issues.len(), 1, "未闭合围栏应作为坏块报出");
let degraded = degrade_mermaid_blocks(content, &issues);
assert!(degraded.contains("```text"), "应降级为 text fence");
assert!(degraded.contains("code-repo-wiki: mermaid parse failed"), "应含降级注释");
let fence_count = degraded.matches("```").count();
assert_eq!(fence_count, 2, "应恰好 1 对围栏(开+闭),实际: {degraded}");
assert!(degraded.ends_with("```\n"), "降级产物应以闭合围栏结尾, 实际: {degraded}");
}
#[test]
fn test_validate_ignores_mermaidx_prefix() {
let content = "```mermaidx\nflowchart LR\nA --> B\n```\n";
assert!(validate_mermaid_blocks(content).is_empty(), "mermaidx 不是 mermaid 块");
}
#[test]
fn test_validate_case_insensitive_fence() {
let content = "```MERMAID\nflowchart LR\nA[unterminated\n```\n";
let issues = validate_mermaid_blocks(content);
assert_eq!(issues.len(), 1, "大写 MERMAID 围栏应被识别");
}
#[test]
fn test_validate_skips_nested_example_fence() {
let content = "```text\n示例:\n```mermaid\nflowchart LR\nA --> B\n```\n```\n";
assert!(validate_mermaid_blocks(content).is_empty(), "text 块内的 mermaid 示例不应被当作真实 mermaid 块");
}
#[test]
fn test_sanitize_message_escapes_comment_terminator() {
let msg = "unexpected token '-->' at line 1";
let cleaned = sanitize_message(msg);
assert!(!cleaned.contains("-->"), "--> 应被替换, 实际: {cleaned}");
assert!(cleaned.contains("-→"), "应含替换后的 -→, 实际: {cleaned}");
}