use std::sync::LazyLock;
use regex::Regex;
static FENCE_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(\s*)(`{3,}|~{3,})([A-Za-z0-9_+.,-]*)\s*$").unwrap());
static ORPHAN_LANG_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^[A-Za-z0-9_+.-]*[A-Za-z0-9_+\-](?:,[A-Za-z0-9_+.-]*[A-Za-z0-9_+\-])*$").unwrap()
});
#[inline]
fn is_null_lang(s: &str) -> bool {
let trimmed = s.trim();
trimmed.is_empty() || trimmed.eq_ignore_ascii_case("null")
}
fn normalize_specifier(line: &str) -> (String, String) {
let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect();
let cleaned = line
.trim()
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(",")
.to_lowercase();
(cleaned, indent)
}
#[must_use]
pub fn compress_fences(lines: &[String]) -> Vec<String> {
lines
.iter()
.map(|line| {
if let Some(cap) = FENCE_RE.captures(line) {
let indent = cap.get(1).map_or("", |m| m.as_str());
let lang = cap.get(3).map_or("", |m| m.as_str());
if is_null_lang(lang) {
format!("{indent}```")
} else {
format!("{indent}```{lang}")
}
} else {
line.clone()
}
})
.collect()
}
fn attach_specifier_to_fence(fence_line: &str, specifier: &str, spec_indent: &str) -> String {
let fence_indent = FENCE_RE
.captures(fence_line)
.and_then(|cap| cap.get(1))
.map_or("", |m| m.as_str());
let final_indent = if fence_indent.is_empty() || spec_indent.starts_with(fence_indent) {
spec_indent
} else {
fence_indent
};
format!("{final_indent}```{specifier}")
}
fn orphan_specifier_target(lines: &[String], start: usize) -> Option<usize> {
let mut index = start;
while index < lines.len() && lines[index].trim().is_empty() {
index += 1;
}
if index >= lines.len() || FENCE_RE.captures(&lines[index]).is_none() {
return None;
}
Some(index)
}
fn orphan_specifier_target_without_language(lines: &[String], start: usize) -> Option<usize> {
let target = orphan_specifier_target(lines, start)?;
let cap = FENCE_RE.captures(&lines[target])?;
let lang = cap.get(3).map_or("", |m| m.as_str());
is_null_lang(lang).then_some(target)
}
#[must_use]
pub fn attach_orphan_specifiers(lines: &[String]) -> Vec<String> {
let mut out = Vec::with_capacity(lines.len());
let mut i = 0;
while i < lines.len() {
let line = &lines[i];
let (spec, indent) = normalize_specifier(line);
if ORPHAN_LANG_RE.is_match(&spec) && out.last().is_none_or(|l: &String| l.trim().is_empty())
{
if let Some(target) = orphan_specifier_target_without_language(lines, i + 1) {
out.push(attach_specifier_to_fence(&lines[target], &spec, &indent));
i = target + 1;
continue;
}
out.push(line.clone());
i += 1;
continue;
}
out.push(line.clone());
i += 1;
}
out
}