poet 0.7.0

Static site generator with optional MCP server and LLM SEO optimizations
Documentation
pub mod metadata_line_item;

use anyhow::Result;
use anyhow::anyhow;
use nom::IResult;
use nom::Parser;
use nom::branch::alt;
use nom::bytes::complete::take_till;
use nom::bytes::complete::take_while1;
use nom::character::complete::char;
use nom::character::complete::multispace1;
use nom::combinator::map;
use nom::multi::separated_list0;
use nom::sequence::delimited;
use nom::sequence::separated_pair;

use crate::parse_markdown_metadata_line::metadata_line_item::MetadataLineItem;

fn identifier(input: &str) -> IResult<&str, &str> {
    take_while1(|c: char| c.is_alphanumeric() || c == '_' || c == '-')(input)
}

fn unquoted_value(input: &str) -> IResult<&str, &str> {
    take_while1(|c: char| !c.is_whitespace() && c != '"' && c != '\'')(input)
}

fn quoted_value(input: &str) -> IResult<&str, &str> {
    alt((
        delimited(char('"'), take_till(|c| c == '"'), char('"')),
        delimited(char('\''), take_till(|c| c == '\''), char('\'')),
    ))
    .parse(input)
}

fn key_value_pair(input: &str) -> IResult<&str, (&str, &str)> {
    separated_pair(identifier, char(':'), alt((quoted_value, unquoted_value))).parse(input)
}

fn meta_item(input: &str) -> IResult<&str, MetadataLineItem> {
    alt((
        map(key_value_pair, |(key, value)| MetadataLineItem::Pair {
            name: key.to_string(),
            value: value.to_string(),
        }),
        map(identifier, |identifier| MetadataLineItem::Flag {
            name: identifier.to_string(),
        }),
    ))
    .parse(input)
}

pub fn parse_markdown_metadata_line(input: &str) -> Result<Vec<MetadataLineItem>> {
    let (rest, items) = separated_list0(multispace1, meta_item)
        .parse(input)
        .map_err(|err| anyhow!("Failed to parse metadata line: {err}"))?;

    if rest.is_empty() {
        Ok(items)
    } else {
        Err(anyhow!(
            "Unexpected traling characters at the end of markdown metadata line: {rest}"
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_metadata() {
        let input = r#"label:"foo bar" class:xD highlighted readonly id:'my-component'"#;
        let result = parse_markdown_metadata_line(input).unwrap();

        assert_eq!(result.len(), 5);

        assert!(result.contains(&MetadataLineItem::Pair {
            name: "label".to_string(),
            value: "foo bar".to_string(),
        }));

        assert!(result.contains(&MetadataLineItem::Pair {
            name: "class".to_string(),
            value: "xD".to_string(),
        }));

        assert!(result.contains(&MetadataLineItem::Pair {
            name: "id".to_string(),
            value: "my-component".to_string(),
        }));

        assert!(result.contains(&MetadataLineItem::Flag {
            name: "highlighted".to_string(),
        }));

        assert!(result.contains(&MetadataLineItem::Flag {
            name: "readonly".to_string(),
        }));
    }
}