use crate::markdown::ast::tokens::Attributes;
pub fn parse_attrs(s: &str) -> Option<(Attributes, usize)> {
let s = s.trim_start();
if !s.starts_with('{') {
return None;
}
let chars: Vec<char> = s.chars().collect();
let mut depth = 0i32;
let mut end = 0usize;
let mut in_string: Option<char> = None;
for (i, &c) in chars.iter().enumerate() {
match in_string {
Some(q) => {
if c == q {
in_string = None;
}
}
None => {
if c == '"' || c == '\'' {
in_string = Some(c);
} else if c == '{' {
depth += 1;
} else if c == '}' {
depth -= 1;
if depth == 0 {
end = i + 1;
break;
}
}
}
}
}
if end == 0 {
return None;
}
let inner_start = if chars.get(1) == Some(&':') { 2 } else { 1 };
let inner: String = chars[inner_start..end - 1].iter().collect();
let attrs = parse_attr_tokens(&inner);
Some((attrs, end))
}
fn parse_attr_tokens(inner: &str) -> Attributes {
let mut attrs = Attributes::default();
let chars: Vec<char> = inner.chars().collect();
let mut i = 0;
while i < chars.len() {
while i < chars.len() && chars[i].is_whitespace() {
i += 1;
}
if i >= chars.len() {
break;
}
match chars[i] {
'#' => {
i += 1;
let start = i;
while i < chars.len() && is_ident_char(chars[i]) {
i += 1;
}
if i > start {
attrs.id = Some(chars[start..i].iter().collect());
}
}
'.' => {
i += 1;
let start = i;
while i < chars.len() && is_ident_char(chars[i]) {
i += 1;
}
if i > start {
let class: String = chars[start..i].iter().collect();
match &mut attrs.classes {
Some(v) => v.push(class),
None => attrs.classes = Some(vec![class]),
}
}
}
c if is_ident_start(c) => {
let key_start = i;
while i < chars.len() && is_ident_char(chars[i]) {
i += 1;
}
let key: String = chars[key_start..i].iter().collect();
while i < chars.len() && chars[i].is_whitespace() {
i += 1;
}
if i < chars.len() && chars[i] == '=' {
i += 1;
while i < chars.len() && chars[i].is_whitespace() {
i += 1;
}
if i < chars.len() && (chars[i] == '"' || chars[i] == '\'') {
let q = chars[i];
i += 1;
let val_start = i;
while i < chars.len() && chars[i] != q {
i += 1;
}
let val: String = chars[val_start..i].iter().collect();
if i < chars.len() {
i += 1; }
push_attr(&mut attrs, key, val);
} else {
let val_start = i;
while i < chars.len() && !chars[i].is_whitespace() {
i += 1;
}
let val: String = chars[val_start..i].iter().collect();
if !val.is_empty() {
push_attr(&mut attrs, key, val);
}
}
} else {
push_attr(&mut attrs, key, String::new());
}
}
_ => {
i += 1; }
}
}
attrs
}
fn push_attr(attrs: &mut Attributes, key: String, val: String) {
match &mut attrs.attributes {
Some(v) => v.push((key, val)),
None => attrs.attributes = Some(vec![(key, val)]),
}
}
fn is_ident_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_' || c == '-'
}
fn is_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '-'
}
#[cfg(test)]
mod tests {
use super::parse_attrs;
#[test]
fn rejects_non_attribute_input() {
assert!(parse_attrs("plain text").is_none());
assert!(parse_attrs("").is_none());
assert!(parse_attrs("{#id").is_none());
}
#[test]
fn parses_id_class_and_key_values() {
let (attrs, consumed) = parse_attrs("{:#hero .card data-role=\"note\"}").unwrap();
assert_eq!(
consumed,
"{:#hero .card data-role=\"note\"}".chars().count()
);
assert_eq!(attrs.id.as_deref(), Some("hero"));
assert_eq!(attrs.classes, Some(vec!["card".to_string()]));
assert_eq!(
attrs.attributes,
Some(vec![("data-role".to_string(), "note".to_string())])
);
}
#[test]
fn colon_after_open_brace_is_optional() {
let (attrs, _) = parse_attrs("{#id}").unwrap();
assert_eq!(attrs.id.as_deref(), Some("id"));
}
#[test]
fn multiple_classes_accumulate() {
let (attrs, _) = parse_attrs("{:.a .b .c}").unwrap();
assert_eq!(
attrs.classes,
Some(vec!["a".to_string(), "b".to_string(), "c".to_string()])
);
}
#[test]
fn quoted_values_may_contain_braces() {
let (attrs, _) = parse_attrs("{:data-json=\"{a:1}\"}").unwrap();
assert_eq!(
attrs.attributes,
Some(vec![("data-json".to_string(), "{a:1}".to_string())])
);
}
#[test]
fn bare_keys_become_boolean_attributes() {
let (attrs, _) = parse_attrs("{:disabled}").unwrap();
assert_eq!(
attrs.attributes,
Some(vec![("disabled".to_string(), String::new())])
);
}
#[test]
fn unquoted_values_read_until_whitespace() {
let (attrs, _) = parse_attrs("{:data-x=1 data-y=2}").unwrap();
assert_eq!(
attrs.attributes,
Some(vec![
("data-x".to_string(), "1".to_string()),
("data-y".to_string(), "2".to_string()),
])
);
}
}