use std::path::Path;
pub fn description(file: &Path) -> Option<String> {
file_field(file, "description")
}
pub fn file_field(file: &Path, key: &str) -> Option<String> {
let text = std::fs::read_to_string(file).ok()?;
field(&text, key)
}
pub fn field(text: &str, key: &str) -> Option<String> {
let mut lines = text.lines().peekable();
if lines.next()?.trim() != "---" {
return None;
}
while let Some(line) = lines.next() {
if line.trim() == "---" {
break; }
if let Some(rest) = line.strip_prefix(key)
&& let Some(value) = rest.strip_prefix(':')
{
let trimmed = value.trim();
if let Some((style, chomp)) = parse_block_indicator(trimmed) {
let mut block_lines: Vec<&str> = Vec::new();
loop {
match lines.peek() {
None => break,
Some(&next) => {
let is_col0_nonempty = !next.is_empty()
&& !next.starts_with(' ')
&& !next.starts_with('\t');
if is_col0_nonempty {
break;
}
lines.next(); block_lines.push(next);
}
}
}
return Some(render_block(&block_lines, style, chomp));
}
return Some(unquote(trimmed));
}
}
None
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum BlockStyle {
Folded,
Literal,
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum Chomp {
Strip,
Clip,
Keep,
}
fn parse_block_indicator(s: &str) -> Option<(BlockStyle, Chomp)> {
let s = s.trim();
let mut chars = s.chars();
let style = match chars.next()? {
'>' => BlockStyle::Folded,
'|' => BlockStyle::Literal,
_ => return None,
};
let chomp = match chars.next() {
None => Chomp::Clip,
Some('-') => Chomp::Strip,
Some('+') => Chomp::Keep,
Some(c) if c.is_whitespace() => Chomp::Clip,
Some('#') => Chomp::Clip,
_ => return None,
};
let rest = chars.as_str().trim();
if !rest.is_empty() && !rest.starts_with('#') {
return None;
}
Some((style, chomp))
}
fn render_block(lines: &[&str], style: BlockStyle, chomp: Chomp) -> String {
let min_indent = lines
.iter()
.filter(|l| !l.trim().is_empty())
.map(|l| leading_spaces(l))
.min()
.unwrap_or(0);
let dedented: Vec<&str> = lines
.iter()
.map(|l| {
if l.len() >= min_indent {
&l[min_indent..]
} else {
""
}
})
.collect();
let value = match style {
BlockStyle::Literal => {
dedented.join("\n")
}
BlockStyle::Folded => fold_lines(&dedented),
};
let value = apply_chomp(&value, chomp);
value.trim().to_string()
}
fn leading_spaces(s: &str) -> usize {
s.bytes().take_while(|&b| b == b' ' || b == b'\t').count()
}
fn fold_lines(lines: &[&str]) -> String {
let mut result = String::new();
let mut in_paragraph = false;
for line in lines {
if line.trim().is_empty() {
result.push('\n');
in_paragraph = false;
} else {
if in_paragraph {
result.push(' ');
}
result.push_str(line);
in_paragraph = true;
}
}
result
}
fn apply_chomp(value: &str, chomp: Chomp) -> String {
match chomp {
Chomp::Strip => value.trim_end_matches('\n').to_string(),
Chomp::Clip => {
let stripped = value.trim_end_matches('\n');
format!("{stripped}\n")
}
Chomp::Keep => value.to_string(),
}
}
fn unquote(s: &str) -> String {
let bytes = s.as_bytes();
if bytes.len() >= 2 {
let first = bytes[0];
let last = bytes[bytes.len() - 1];
if (first == b'"' && last == b'"') || (first == b'\'' && last == b'\'') {
return s[1..s.len() - 1].to_string();
}
}
s.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_plain_description() {
let t = "---\nname: review\ndescription: Review the diff\n---\n# body\n";
assert_eq!(field(t, "description").as_deref(), Some("Review the diff"));
}
#[test]
fn strips_quotes_double() {
let t = "---\ndescription: \"quoted value\"\n---\n";
assert_eq!(field(t, "description").as_deref(), Some("quoted value"));
}
#[test]
fn strips_quotes_single() {
let t = "---\ndescription: 'single quoted'\n---\n";
assert_eq!(field(t, "description").as_deref(), Some("single quoted"));
}
#[test]
fn none_without_frontmatter() {
assert_eq!(field("# just a heading\n", "description"), None);
}
#[test]
fn stops_at_closing_delimiter() {
let t = "---\nname: x\n---\ndescription: not in frontmatter\n";
assert_eq!(field(t, "description"), None);
}
#[test]
fn folded_strip_joins_with_spaces_no_trailing_newline() {
let t = "---\ndescription: >-\n First line\n second line\n third line\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "First line second line third line");
assert!(!result.ends_with('\n'));
}
#[test]
fn folded_clip_joins_with_spaces() {
let t = "---\ndescription: >\n Hello\n world\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "Hello world");
}
#[test]
fn literal_clip_preserves_newlines() {
let t = "---\ndescription: |\n line one\n line two\n---\n";
let result = field(t, "description").unwrap();
assert!(
result.contains('\n'),
"expected internal newline, got: {result:?}"
);
let parts: Vec<&str> = result.lines().collect();
assert_eq!(parts, vec!["line one", "line two"]);
}
#[test]
fn literal_strip_no_trailing_newline() {
let t = "---\ndescription: |-\n alpha\n beta\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "alpha\nbeta");
assert!(!result.ends_with('\n'));
}
#[test]
fn literal_keep_chomping_parses_without_error() {
let t = "---\ndescription: |+\n only line\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "only line");
}
#[test]
fn folded_keep_chomping_parses_without_error() {
let t = "---\ndescription: >+\n only line\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "only line");
}
#[test]
fn block_ends_at_next_key() {
let t = "---\ndescription: >-\n Block text here\nauthor: Alice\n---\n";
let desc = field(t, "description").unwrap();
assert_eq!(desc, "Block text here");
let author = field(t, "author").unwrap();
assert_eq!(author, "Alice");
}
#[test]
fn block_ends_at_closing_delimiter() {
let t = "---\ndescription: |-\n Just this\n---\nbody text\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "Just this");
}
#[test]
fn folded_blank_line_becomes_paragraph_break() {
let t = "---\ndescription: >-\n First paragraph\n\n Second paragraph\n---\n";
let result = field(t, "description").unwrap();
assert!(
result.contains('\n'),
"expected paragraph break newline, got: {result:?}"
);
let parts: Vec<&str> = result.lines().collect();
assert_eq!(parts, vec!["First paragraph", "Second paragraph"]);
}
#[test]
fn block_scalar_on_arbitrary_key() {
let t = "---\nbuild: |-\n cargo build\n --release\n---\n";
let result = field(t, "build").unwrap();
assert_eq!(result, "cargo build\n--release");
}
#[test]
fn block_dedents_minimum_indentation() {
let t = "---\ndescription: >-\n deeper indent\n continues here\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "deeper indent continues here");
}
#[test]
fn indicator_folded_clip() {
let (style, chomp) = parse_block_indicator(">").unwrap();
assert_eq!(style, BlockStyle::Folded);
assert_eq!(chomp, Chomp::Clip);
}
#[test]
fn indicator_folded_strip() {
let (style, chomp) = parse_block_indicator(">-").unwrap();
assert_eq!(style, BlockStyle::Folded);
assert_eq!(chomp, Chomp::Strip);
}
#[test]
fn indicator_literal_keep() {
let (style, chomp) = parse_block_indicator("|+").unwrap();
assert_eq!(style, BlockStyle::Literal);
assert_eq!(chomp, Chomp::Keep);
}
#[test]
fn indicator_rejects_plain_scalar() {
assert!(parse_block_indicator("some text").is_none());
assert!(parse_block_indicator("\"quoted\"").is_none());
}
#[test]
fn indicator_rejects_extra_chars() {
assert!(parse_block_indicator(">- extra").is_none());
}
#[test]
fn folded_first_content_line_blank() {
let t = "---\ndescription: >-\n\n real text here\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "real text here");
}
#[test]
fn literal_first_content_line_blank() {
let t = "---\ndescription: |-\n\n kept line\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "kept line");
}
#[test]
fn block_of_only_blank_lines_trims_to_empty() {
let t = "---\ndescription: >-\n\n \n\nauthor: Bob\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "");
assert_eq!(field(t, "author").as_deref(), Some("Bob"));
}
#[test]
fn literal_block_of_only_blank_lines_trims_to_empty() {
let t = "---\ndescription: |\n\n\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "");
}
#[test]
fn literal_mixed_indentation_preserves_extra_after_dedent() {
let t = "---\ndescription: |-\n top\n nested\n back\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "top\n nested\nback");
}
#[test]
fn folded_indicator_with_trailing_comment() {
let t = "---\ndescription: >- # a note\n hello there\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "hello there");
}
#[test]
fn literal_indicator_with_trailing_comment() {
let t = "---\ndescription: | # keep literal\n one\n two\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "one\ntwo");
}
#[test]
fn folded_block_is_last_key_before_closing_delimiter() {
let t = "---\nname: x\ndescription: >-\n final folded value\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "final folded value");
}
#[test]
fn folded_block_tolerates_crlf() {
let t = "---\r\ndescription: >-\r\n alpha\r\n beta\r\n---\r\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "alpha beta");
}
#[test]
fn folded_block_tolerates_trailing_spaces() {
let t = "---\ndescription: >-\n alpha \n beta \n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "alpha beta");
}
#[test]
fn malformed_indicator_falls_back_to_plain_scalar() {
let t = "---\ndescription: >x\n not part of a block\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, ">x");
assert!(parse_block_indicator(">x").is_none());
}
#[test]
fn indicator_with_extra_text_reads_as_plain_scalar() {
let t = "---\ndescription: > extra words\n---\n";
let result = field(t, "description").unwrap();
assert_eq!(result, "> extra words");
assert!(parse_block_indicator("> extra words").is_none());
assert!(parse_block_indicator(">x").is_none());
}
}