use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrontmatterFormat {
Yaml,
Toml,
Custom(&'static str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocumentWithFrontmatter<'a> {
pub frontmatter: Option<&'a str>,
pub content: &'a str,
pub format: Option<FrontmatterFormat>,
}
pub fn split_frontmatter(input: &str) -> DocumentWithFrontmatter<'_> {
let trimmed = input.trim_start_matches('\u{feff}');
let (delim, format) = if trimmed.starts_with("---\r\n") || trimmed.starts_with("---\n") || trimmed == "---" {
("---", FrontmatterFormat::Yaml)
} else if trimmed.starts_with("+++\r\n") || trimmed.starts_with("+++\n") || trimmed == "+++" {
("+++", FrontmatterFormat::Toml)
} else {
return DocumentWithFrontmatter {
frontmatter: None,
content: trimmed,
format: None,
};
};
let first_line_end = match trimmed.find('\n') {
Some(idx) => idx + 1,
None => {
return DocumentWithFrontmatter {
frontmatter: None,
content: trimmed,
format: None,
};
}
};
let rest = &trimmed[first_line_end..];
let mut offset = 0;
while offset < rest.len() {
let line_slice = &rest[offset..];
let line_len = line_slice.find('\n').map(|i| i + 1).unwrap_or(line_slice.len());
let current_line = &line_slice[..line_len];
let trimmed_line = current_line.trim_end_matches(&['\r', '\n'][..]);
let is_closing = if format == FrontmatterFormat::Yaml {
trimmed_line == delim || trimmed_line == "..."
} else {
trimmed_line == delim
};
if is_closing {
let fm_raw = &rest[..offset];
let fm = fm_raw.trim_end_matches(&['\r', '\n'][..]);
let content_start = offset + line_len;
let content = if content_start <= rest.len() {
let rem = &rest[content_start..];
rem
} else {
""
};
return DocumentWithFrontmatter {
frontmatter: Some(fm),
content,
format: Some(format),
};
}
offset += line_len;
}
DocumentWithFrontmatter {
frontmatter: None,
content: trimmed,
format: None,
}
}
pub fn indent(text: &str, prefix: &str) -> String {
let mut result = String::with_capacity(text.len() + prefix.len() * 4);
let ends_with_newline = text.ends_with('\n');
let content = if ends_with_newline {
&text[..text.len() - 1]
} else {
text
};
let mut first = true;
for line in content.split('\n') {
let line_clean = line.strip_suffix('\r').unwrap_or(line);
if !first {
result.push('\n');
}
first = false;
if !line_clean.is_empty() {
result.push_str(prefix);
result.push_str(line_clean);
}
}
if ends_with_newline {
result.push('\n');
}
result
}
pub fn dedent(text: &str) -> String {
let ends_with_newline = text.ends_with('\n');
let content = if ends_with_newline {
&text[..text.len() - 1]
} else {
text
};
let lines: Vec<&str> = content
.split('\n')
.map(|l| l.strip_suffix('\r').unwrap_or(l))
.collect();
let mut min_indent: Option<usize> = None;
for line in &lines {
if line.trim().is_empty() {
continue;
}
let indent = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
min_indent = Some(match min_indent {
Some(curr) => curr.min(indent),
None => indent,
});
}
let cut = min_indent.unwrap_or(0);
if cut == 0 {
return text.into();
}
let mut result = String::with_capacity(text.len());
let mut first = true;
for line in lines {
if !first {
result.push('\n');
}
first = false;
if line.trim().is_empty() {
} else if line.len() >= cut {
result.push_str(&line[cut..]);
} else {
result.push_str(line);
}
}
if ends_with_newline {
result.push('\n');
}
result
}
pub fn trim_lines(text: &str) -> String {
let mut lines: Vec<&str> = text
.split('\n')
.map(|l| l.strip_suffix('\r').unwrap_or(l).trim_end())
.collect();
while !lines.is_empty() && lines[0].is_empty() {
lines.remove(0);
}
while !lines.is_empty() && lines[lines.len() - 1].is_empty() {
lines.pop();
}
lines.join("\n")
}
pub fn line_count(text: &str) -> usize {
if text.is_empty() {
return 0;
}
text.split('\n').count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split_frontmatter_yaml() {
let input = "---\ntitle: Rust Guide\nauthor: Ferris\n---\n# Chapter 1\nHello world.";
let doc = split_frontmatter(input);
assert_eq!(doc.format, Some(FrontmatterFormat::Yaml));
assert_eq!(doc.frontmatter, Some("title: Rust Guide\nauthor: Ferris"));
assert_eq!(doc.content, "# Chapter 1\nHello world.");
}
#[test]
fn test_split_frontmatter_yaml_with_crlf() {
let input = "---\r\ntitle: Windows\r\n---\r\nBody text";
let doc = split_frontmatter(input);
assert_eq!(doc.format, Some(FrontmatterFormat::Yaml));
assert_eq!(doc.frontmatter, Some("title: Windows"));
assert_eq!(doc.content, "Body text");
}
#[test]
fn test_split_frontmatter_toml() {
let input = "+++\ntitle = \"Config\"\n+++\nContent here";
let doc = split_frontmatter(input);
assert_eq!(doc.format, Some(FrontmatterFormat::Toml));
assert_eq!(doc.frontmatter, Some("title = \"Config\""));
assert_eq!(doc.content, "Content here");
}
#[test]
fn test_split_frontmatter_none() {
let input = "Just markdown content\nwith multiple lines.";
let doc = split_frontmatter(input);
assert_eq!(doc.format, None);
assert_eq!(doc.frontmatter, None);
assert_eq!(doc.content, input);
}
#[test]
fn test_indent() {
let text = "alpha\nbeta\n\ngamma";
let indented = indent(text, " ");
assert_eq!(indented, " alpha\n beta\n\n gamma");
}
#[test]
fn test_dedent() {
let text = " func hello() {\n return 42;\n }";
let dedented = dedent(text);
assert_eq!(dedented, "func hello() {\n return 42;\n}");
}
#[test]
fn test_trim_lines() {
let text = "\n\n hello world \n foo bar \t \n\n\n";
let trimmed = trim_lines(text);
assert_eq!(trimmed, " hello world\n foo bar");
}
#[test]
fn test_line_count() {
assert_eq!(line_count(""), 0);
assert_eq!(line_count("hello"), 1);
assert_eq!(line_count("hello\nworld"), 2);
}
}