#[derive(Debug, Clone, Eq, PartialEq)]
pub enum BlockAttr {
Id(String),
Class(String),
KeyValue(String, String),
}
impl BlockAttr {
pub fn parse(input: &str) -> Result<Vec<Self>, BlockAttrError> {
let s = input.trim();
if let Some(s) = s.strip_prefix('{') {
if let Some(s) = s.strip_suffix('}') {
let attrs: Result<Vec<Self>, BlockAttrError> =
s.split_ascii_whitespace().map(Self::parse_one).collect();
let attrs = attrs?;
if attrs
.iter()
.filter(|a| matches!(a, BlockAttr::Id(_)))
.count()
> 1
{
Err(BlockAttrError::MoreThanOneId(input.into()))
} else {
Ok(attrs)
}
} else {
Err(BlockAttrError::NoCloseBrace(s.into()))
}
} else {
let words: Vec<&str> = s.split_ascii_whitespace().collect();
if words.is_empty() {
Ok(vec![])
} else if words.len() == 1 {
Ok(vec![Self::Class(words[0].into())])
} else {
Err(BlockAttrError::Words(s.into()))
}
}
}
fn parse_one(s: &str) -> Result<Self, BlockAttrError> {
if let Some(s) = s.strip_prefix('.') {
Ok(Self::Class(s.into()))
} else if let Some(s) = s.strip_prefix('#') {
Ok(Self::Id(s.into()))
} else if let Some((k, v)) = s.split_once('=') {
if v.starts_with('"') || v.starts_with('\'') {
Err(BlockAttrError::QuotedValue(s.into()))
} else {
Ok(Self::KeyValue(k.into(), v.into()))
}
} else {
Ok(Self::Class(s.into()))
}
}
}
#[derive(Debug, thiserror::Error, Eq, PartialEq)]
pub enum BlockAttrError {
#[error("fenced code block attribute has multiple words: {0:?}")]
Words(String),
#[error("fenced code block attributes lacks close brace: {0:?}")]
NoCloseBrace(String),
#[error("fenced code block has more than one identifier: {0:?}")]
MoreThanOneId(String),
#[error("fenced code block has quoted value: {0:?}")]
QuotedValue(String),
}
#[cfg(test)]
mod test {
use crate::blockattr::BlockAttrError;
use super::BlockAttr;
#[test]
fn empty_string() {
assert_eq!(BlockAttr::parse("").unwrap(), vec![]);
}
#[test]
fn just_word() {
assert_eq!(
BlockAttr::parse("file").unwrap(),
vec![BlockAttr::Class("file".into())]
);
}
#[test]
fn two_words() {
assert!(matches!(
BlockAttr::parse("haskell file"),
Err(BlockAttrError::Words(_))
));
}
#[test]
fn open_brace_without_close() {
assert!(matches!(
BlockAttr::parse("{foo"),
Err(BlockAttrError::NoCloseBrace(_))
));
}
#[test]
fn empty_braces() {
assert_eq!(BlockAttr::parse("{}").unwrap(), vec![]);
}
#[test]
fn two_ids() {
assert_eq!(
BlockAttr::parse("{#foo #bar}"),
Err(BlockAttrError::MoreThanOneId("{#foo #bar}".into()))
);
}
#[test]
fn parse_one_word() {
assert_eq!(
BlockAttr::parse_one("foo"),
Ok(BlockAttr::Class("foo".into()))
);
}
#[test]
fn parse_one_dotted_word() {
assert_eq!(
BlockAttr::parse_one(".foo"),
Ok(BlockAttr::Class("foo".into()))
);
}
#[test]
fn parse_one_id() {
assert_eq!(
BlockAttr::parse_one("#foo"),
Ok(BlockAttr::Id("foo".into()))
);
}
#[test]
fn parse_one_kv() {
assert_eq!(
BlockAttr::parse_one("foo=bar"),
Ok(BlockAttr::KeyValue("foo".into(), "bar".into()))
);
}
#[test]
fn parse_one_kv_with_double_quotes() {
assert!(matches!(
BlockAttr::parse_one(r#"foo="bar""#),
Err(BlockAttrError::QuotedValue(_))
));
}
#[test]
fn parse_one_kv_with_single_quotes() {
assert!(matches!(
BlockAttr::parse_one("foo='bar'"),
Err(BlockAttrError::QuotedValue(_))
));
}
}