use regex::Regex;
use std::sync::LazyLock;
static SPAN_IAL_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\{[:\.#][^}]*\}$").unwrap());
static EXTENSION_OPEN_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^\s*\{::([a-z]+)(?:\s+[^}]*)?\}\s*$").unwrap());
static EXTENSION_SELF_CLOSING_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^\s*\{::[a-z]+(?:\s+[^}]*)?\s*/\}\s*$").unwrap());
static EXTENSION_CLOSE_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*\{:/([a-z]+)?\}\s*$").unwrap());
static MATH_BLOCK_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\$\$").unwrap());
pub fn is_kramdown_block_attribute(line: &str) -> bool {
let trimmed = line.trim();
if !trimmed.starts_with('{') || !trimmed.ends_with('}') || trimmed.len() < 3 {
return false;
}
let second_char = trimmed.chars().nth(1);
matches!(second_char, Some(':') | Some('#') | Some('.'))
}
pub fn has_span_ial(text: &str) -> bool {
SPAN_IAL_PATTERN.is_match(text.trim())
}
pub fn is_kramdown_extension_self_closing(line: &str) -> bool {
EXTENSION_SELF_CLOSING_PATTERN.is_match(line)
}
pub fn is_kramdown_extension_open(line: &str) -> bool {
EXTENSION_OPEN_PATTERN.is_match(line) && !is_kramdown_extension_self_closing(line)
}
pub fn is_kramdown_extension_close(line: &str) -> bool {
EXTENSION_CLOSE_PATTERN.is_match(line)
}
pub fn is_math_block_delimiter(line: &str) -> bool {
let trimmed = line.trim();
trimmed == "$$" || MATH_BLOCK_PATTERN.is_match(trimmed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kramdown_class_attributes() {
assert!(is_kramdown_block_attribute("{:.wrap}"));
assert!(is_kramdown_block_attribute("{:.class-name}"));
assert!(is_kramdown_block_attribute("{:.multiple .classes}"));
}
#[test]
fn test_kramdown_id_attributes() {
assert!(is_kramdown_block_attribute("{:#my-id}"));
assert!(is_kramdown_block_attribute("{:#section-1}"));
}
#[test]
fn test_kramdown_generic_attributes() {
assert!(is_kramdown_block_attribute("{:style=\"color: red\"}"));
assert!(is_kramdown_block_attribute("{:data-value=\"123\"}"));
}
#[test]
fn test_kramdown_combined_attributes() {
assert!(is_kramdown_block_attribute("{:.class #id}"));
assert!(is_kramdown_block_attribute("{:#id .class style=\"color: blue\"}"));
assert!(is_kramdown_block_attribute("{:.wrap #my-code .highlight}"));
}
#[test]
fn test_non_kramdown_braces() {
assert!(!is_kramdown_block_attribute("{just some text}"));
assert!(!is_kramdown_block_attribute("{not kramdown}"));
assert!(!is_kramdown_block_attribute("{ spaces }"));
}
#[test]
fn test_edge_cases() {
assert!(!is_kramdown_block_attribute("{}"));
assert!(!is_kramdown_block_attribute("{"));
assert!(!is_kramdown_block_attribute("}"));
assert!(!is_kramdown_block_attribute(""));
assert!(!is_kramdown_block_attribute("not braces"));
}
#[test]
fn test_whitespace_handling() {
assert!(is_kramdown_block_attribute(" {:.wrap} "));
assert!(is_kramdown_block_attribute("\t{:#id}\t"));
assert!(is_kramdown_block_attribute(" {:.class #id} "));
}
#[test]
fn test_self_closing_extension_blocks() {
assert!(is_kramdown_extension_self_closing("{::options toc_levels=\"2..4\" /}"));
assert!(is_kramdown_extension_self_closing("{::comment /}"));
assert!(is_kramdown_extension_self_closing("{::nomarkdown this='is' .ignore /}"));
assert!(is_kramdown_extension_self_closing(" {::options key=\"val\" /} "));
assert!(!is_kramdown_extension_self_closing("{::comment}"));
assert!(!is_kramdown_extension_self_closing("{::nomarkdown}"));
assert!(!is_kramdown_extension_self_closing("{::nomarkdown type='html'}"));
}
#[test]
fn test_extension_open_excludes_self_closing() {
assert!(is_kramdown_extension_open("{::comment}"));
assert!(is_kramdown_extension_open("{::nomarkdown}"));
assert!(is_kramdown_extension_open("{::nomarkdown type='html'}"));
assert!(!is_kramdown_extension_open("{::options toc_levels=\"2..4\" /}"));
assert!(!is_kramdown_extension_open("{::comment /}"));
}
}