#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] enum LineContext {
Normal,
InCodeFence,
InIgnoreBlock,
}
#[allow(dead_code)] fn is_ignore_start(line: &str) -> bool {
let trimmed = line.trim();
trimmed.contains("<!-- ascfix:ignore -->") || trimmed.contains("<!-- ascfix-ignore-start -->")
}
#[allow(dead_code)] fn is_ignore_end(line: &str) -> bool {
let trimmed = line.trim();
trimmed.contains("<!-- /ascfix:ignore -->") || trimmed.contains("<!-- ascfix-ignore-end -->")
}
#[allow(dead_code)] fn parse_line_contexts(text: &str) -> Vec<(usize, &str, LineContext)> {
let mut result = Vec::new();
let mut in_fence = false;
let mut in_ignore = false;
for (line_num, line) in text.lines().enumerate() {
if is_ignore_start(line) {
in_ignore = true;
continue;
} else if is_ignore_end(line) {
in_ignore = false;
continue;
}
if in_ignore {
result.push((line_num, line, LineContext::InIgnoreBlock));
continue;
}
let backtick_count = line.matches("```").count();
let tilde_count = line.matches("~~~").count();
let is_fence_marker = backtick_count > 0 || tilde_count > 0;
if backtick_count % 2 == 1 || tilde_count % 2 == 1 {
in_fence = !in_fence;
}
let line_ctx = if is_fence_marker {
continue;
} else if in_fence {
LineContext::InCodeFence
} else {
LineContext::Normal
};
result.push((line_num, line, line_ctx));
}
result
}
#[allow(dead_code)] #[must_use]
pub fn extract_normal_lines(text: &str) -> Vec<(usize, String)> {
parse_line_contexts(text)
.into_iter()
.filter_map(|(line_num, line, line_ctx)| {
if line_ctx == LineContext::Normal {
Some((line_num, line.to_string()))
} else {
None
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ignores_content_in_backtick_fence() {
let markdown = "# Header\n\n```\ncode line\nmore code\n```\n\nAfter fence";
let normal_lines = extract_normal_lines(markdown);
let line_texts: Vec<&str> = normal_lines.iter().map(|(_, l)| l.as_str()).collect();
assert!(!line_texts.contains(&"code line"));
assert!(!line_texts.contains(&"more code"));
assert!(line_texts.contains(&"# Header"));
assert!(line_texts.contains(&"After fence"));
}
#[test]
fn test_ignores_content_in_tilde_fence() {
let markdown = "Before\n\n~~~\nfenced content\n~~~\n\nAfter";
let normal_lines = extract_normal_lines(markdown);
let line_texts: Vec<&str> = normal_lines.iter().map(|(_, l)| l.as_str()).collect();
assert!(!line_texts.contains(&"fenced content"));
assert!(line_texts.contains(&"Before"));
assert!(line_texts.contains(&"After"));
}
#[test]
fn test_preserves_line_numbers() {
let markdown = "Line 0\n\n```\nLine 3 (hidden)\n```\n\nLine 6";
let normal_lines = extract_normal_lines(markdown);
let line_nums: Vec<usize> = normal_lines.iter().map(|(num, _)| *num).collect();
assert_eq!(line_nums, vec![0, 1, 5, 6]);
}
#[test]
fn test_nested_triple_backticks_in_content() {
let markdown = "text\n```\nhidden\n```\nvisible";
let normal_lines = extract_normal_lines(markdown);
let line_texts: Vec<&str> = normal_lines.iter().map(|(_, l)| l.as_str()).collect();
assert!(line_texts.contains(&"text"));
assert!(line_texts.contains(&"visible"));
assert!(!line_texts.contains(&"hidden"));
}
#[test]
fn test_multiple_code_blocks() {
let markdown = "Before\n\n```\nBlock1\n```\n\nMiddle\n\n```\nBlock2\n```\n\nAfter";
let normal_lines = extract_normal_lines(markdown);
let line_texts: Vec<&str> = normal_lines.iter().map(|(_, l)| l.as_str()).collect();
assert!(!line_texts.contains(&"Block1"));
assert!(!line_texts.contains(&"Block2"));
assert!(line_texts.contains(&"Before"));
assert!(line_texts.contains(&"Middle"));
assert!(line_texts.contains(&"After"));
}
#[test]
fn test_empty_markdown() {
let markdown = "";
let normal_lines = extract_normal_lines(markdown);
assert!(normal_lines.is_empty());
}
#[test]
fn test_no_code_blocks() {
let markdown = "# Header\n\nSome text\nMore text";
let normal_lines = extract_normal_lines(markdown);
assert_eq!(normal_lines.len(), 4);
}
#[test]
fn test_ignore_block_basic() {
let markdown =
"Before\n<!-- ascfix:ignore -->\nIgnored content\n<!-- /ascfix:ignore -->\nAfter";
let normal_lines = extract_normal_lines(markdown);
let line_texts: Vec<&str> = normal_lines.iter().map(|(_, l)| l.as_str()).collect();
assert!(line_texts.contains(&"Before"));
assert!(line_texts.contains(&"After"));
assert!(!line_texts.contains(&"Ignored content"));
}
#[test]
fn test_ignore_block_alternative_syntax() {
let markdown =
"Before\n<!-- ascfix-ignore-start -->\nIgnored\n<!-- ascfix-ignore-end -->\nAfter";
let normal_lines = extract_normal_lines(markdown);
let line_texts: Vec<&str> = normal_lines.iter().map(|(_, l)| l.as_str()).collect();
assert!(line_texts.contains(&"Before"));
assert!(line_texts.contains(&"After"));
assert!(!line_texts.contains(&"Ignored"));
}
#[test]
fn test_ignore_block_multiple() {
let markdown = "Text1\n<!-- ascfix:ignore -->\nIgnored1\n<!-- /ascfix:ignore -->\nText2\n<!-- ascfix:ignore -->\nIgnored2\n<!-- /ascfix:ignore -->\nText3";
let normal_lines = extract_normal_lines(markdown);
let line_texts: Vec<&str> = normal_lines.iter().map(|(_, l)| l.as_str()).collect();
assert!(line_texts.contains(&"Text1"));
assert!(line_texts.contains(&"Text2"));
assert!(line_texts.contains(&"Text3"));
assert!(!line_texts.contains(&"Ignored1"));
assert!(!line_texts.contains(&"Ignored2"));
}
#[test]
fn test_ignore_block_multiline() {
let markdown =
"Before\n<!-- ascfix:ignore -->\nLine1\nLine2\nLine3\n<!-- /ascfix:ignore -->\nAfter";
let normal_lines = extract_normal_lines(markdown);
let line_texts: Vec<&str> = normal_lines.iter().map(|(_, l)| l.as_str()).collect();
assert_eq!(line_texts.len(), 2);
assert!(line_texts.contains(&"Before"));
assert!(line_texts.contains(&"After"));
}
#[test]
fn test_ignore_block_with_code_fence() {
let markdown =
"Text\n<!-- ascfix:ignore -->\n```\ncode\n```\n<!-- /ascfix:ignore -->\nAfter";
let normal_lines = extract_normal_lines(markdown);
let line_texts: Vec<&str> = normal_lines.iter().map(|(_, l)| l.as_str()).collect();
assert!(line_texts.contains(&"Text"));
assert!(line_texts.contains(&"After"));
assert!(!line_texts.contains(&"code"));
}
#[test]
fn test_code_fence_with_ignore_markers_inside() {
let markdown =
"Before\n```\n<!-- ascfix:ignore -->\ncode\n<!-- /ascfix:ignore -->\n```\nAfter";
let normal_lines = extract_normal_lines(markdown);
let line_texts: Vec<&str> = normal_lines.iter().map(|(_, l)| l.as_str()).collect();
assert!(line_texts.contains(&"Before"));
assert!(line_texts.contains(&"After"));
assert!(!line_texts.contains(&"code"));
assert!(!line_texts.contains(&"<!-- ascfix:ignore -->"));
}
#[test]
fn test_ignore_start_detection() {
assert!(is_ignore_start("<!-- ascfix:ignore -->"));
assert!(is_ignore_start(" <!-- ascfix:ignore --> "));
assert!(is_ignore_start("<!-- ascfix-ignore-start -->"));
assert!(is_ignore_start(" <!-- ascfix-ignore-start --> "));
assert!(!is_ignore_start("<!-- /ascfix:ignore -->"));
assert!(!is_ignore_start("<!-- ascfix-ignore-end -->"));
assert!(!is_ignore_start("regular text"));
}
#[test]
fn test_ignore_end_detection() {
assert!(is_ignore_end("<!-- /ascfix:ignore -->"));
assert!(is_ignore_end(" <!-- /ascfix:ignore --> "));
assert!(is_ignore_end("<!-- ascfix-ignore-end -->"));
assert!(is_ignore_end(" <!-- ascfix-ignore-end --> "));
assert!(!is_ignore_end("<!-- ascfix:ignore -->"));
assert!(!is_ignore_end("<!-- ascfix-ignore-start -->"));
assert!(!is_ignore_end("regular text"));
}
#[test]
fn test_ignore_block_preserves_line_numbers() {
let markdown =
"Line0\n<!-- ascfix:ignore -->\nLine2\nLine3\n<!-- /ascfix:ignore -->\nLine5";
let normal_lines = extract_normal_lines(markdown);
let line_nums: Vec<usize> = normal_lines.iter().map(|(num, _)| *num).collect();
assert_eq!(line_nums, vec![0, 5]);
}
#[test]
fn test_unclosed_ignore_block() {
let markdown = "Before\n<!-- ascfix:ignore -->\nIgnored1\nIgnored2\nAlso ignored";
let normal_lines = extract_normal_lines(markdown);
let line_texts: Vec<&str> = normal_lines.iter().map(|(_, l)| l.as_str()).collect();
assert_eq!(line_texts.len(), 1);
assert!(line_texts.contains(&"Before"));
}
}