#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FenceParts {
pub language: String,
pub body: String,
}
pub fn extract_fence(text: &str) -> Option<FenceParts> {
let mut lines = text.split('\n');
let first = lines.next()?.trim_start();
let after_ticks = first.strip_prefix("```")?;
let language = after_ticks
.split_whitespace()
.next()
.unwrap_or("")
.to_ascii_lowercase();
let mut body = String::new();
let mut first_body_line = true;
for line in lines {
if line.trim_start().starts_with("```") {
break; }
if !first_body_line {
body.push('\n');
}
body.push_str(line);
first_body_line = false;
}
Some(FenceParts { language, body })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_simple_lisp_block() {
let parts = extract_fence("```lisp\n(+ 1 2)\n```").unwrap();
assert_eq!(parts.language, "lisp");
assert_eq!(parts.body, "(+ 1 2)");
}
#[test]
fn extracts_multi_line_body() {
let parts = extract_fence("```python\nfor i in range(3):\n print(i)\n```").unwrap();
assert_eq!(parts.language, "python");
assert_eq!(parts.body, "for i in range(3):\n print(i)");
}
#[test]
fn language_lowercased() {
let parts = extract_fence("```LISP\n(+ 1 2)\n```").unwrap();
assert_eq!(parts.language, "lisp");
}
#[test]
fn empty_language_when_bare_fence() {
let parts = extract_fence("```\nplain text\n```").unwrap();
assert_eq!(parts.language, "");
assert_eq!(parts.body, "plain text");
}
#[test]
fn returns_none_for_non_fence_block() {
assert!(extract_fence("just regular text").is_none());
assert!(extract_fence("- bullet content").is_none());
}
#[test]
fn handles_info_string_with_extra_attrs() {
let parts = extract_fence("```python {.numberLines}\nprint(1)\n```").unwrap();
assert_eq!(parts.language, "python");
}
#[test]
fn missing_closer_is_tolerated() {
let parts = extract_fence("```lisp\n(+ 1 2)\n").unwrap();
assert_eq!(parts.body, "(+ 1 2)\n");
}
}