1use crate::types::{AttrValue, Attrs, Span};
2use crate::error::ParseError;
3
4pub fn parse_attrs(input: &str) -> Result<Attrs, ParseError> {
14 let trimmed = input.trim();
15
16 let inner = if trimmed.starts_with('[') && trimmed.ends_with(']') {
18 &trimmed[1..trimmed.len() - 1]
19 } else {
20 trimmed
21 };
22
23 let chars: Vec<char> = inner.chars().collect();
24 let len = chars.len();
25 let mut pos = 0;
26 let mut attrs = Attrs::new();
27
28 while pos < len {
29 while pos < len && chars[pos].is_whitespace() {
31 pos += 1;
32 }
33 if pos >= len {
34 break;
35 }
36
37 let key_start = pos;
39 while pos < len && (chars[pos].is_alphanumeric() || chars[pos] == '-' || chars[pos] == '_')
40 {
41 pos += 1;
42 }
43
44 if pos == key_start {
45 return Err(ParseError::InvalidAttrs {
46 message: format!("unexpected character '{}' at position {}", chars[pos], pos),
47 span: Span {
48 start_line: 0,
49 end_line: 0,
50 start_offset: pos,
51 end_offset: pos + 1,
52 },
53 });
54 }
55
56 let key: String = chars[key_start..pos].iter().collect();
57
58 if pos < len && chars[pos] == '=' {
60 pos += 1; if pos >= len {
63 return Err(ParseError::InvalidAttrs {
64 message: format!("missing value after '=' for key '{key}'"),
65 span: Span {
66 start_line: 0,
67 end_line: 0,
68 start_offset: pos,
69 end_offset: pos,
70 },
71 });
72 }
73
74 if chars[pos] == '"' {
75 pos += 1; let mut value = String::new();
78 while pos < len && chars[pos] != '"' {
79 if chars[pos] == '\\' && pos + 1 < len {
80 let next = chars[pos + 1];
81 match next {
82 '"' | '\\' => {
83 value.push(next);
84 pos += 2;
85 }
86 _ => {
87 value.push(chars[pos]);
88 pos += 1;
89 }
90 }
91 } else {
92 value.push(chars[pos]);
93 pos += 1;
94 }
95 }
96 if pos < len && chars[pos] == '"' {
97 pos += 1; } else {
99 return Err(ParseError::InvalidAttrs {
100 message: format!("unterminated quoted value for key '{key}'"),
101 span: Span {
102 start_line: 0,
103 end_line: 0,
104 start_offset: key_start,
105 end_offset: pos,
106 },
107 });
108 }
109 attrs.insert(key, AttrValue::String(value));
110 } else {
111 let val_start = pos;
113 while pos < len && !chars[pos].is_whitespace() && chars[pos] != ']' {
114 pos += 1;
115 }
116 let raw: String = chars[val_start..pos].iter().collect();
117 attrs.insert(key, coerce_value(&raw));
118 }
119 } else {
120 attrs.insert(key, AttrValue::Bool(true));
122 }
123 }
124
125 Ok(attrs)
126}
127
128fn coerce_value(raw: &str) -> AttrValue {
131 match raw {
132 "true" => AttrValue::Bool(true),
133 "false" => AttrValue::Bool(false),
134 "null" => AttrValue::Null,
135 _ => {
136 if let Ok(n) = raw.parse::<f64>() {
137 AttrValue::Number(n)
139 } else {
140 AttrValue::String(raw.to_string())
141 }
142 }
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use pretty_assertions::assert_eq;
150
151 #[test]
152 fn parse_empty_attrs() {
153 let attrs = parse_attrs("[]").unwrap();
154 assert!(attrs.is_empty());
155 }
156
157 #[test]
158 fn parse_single_unquoted() {
159 let attrs = parse_attrs("[key=value]").unwrap();
160 assert_eq!(attrs.len(), 1);
161 assert_eq!(attrs["key"], AttrValue::String("value".into()));
162 }
163
164 #[test]
165 fn parse_quoted_value() {
166 let attrs = parse_attrs(r#"[key="hello world"]"#).unwrap();
167 assert_eq!(attrs["key"], AttrValue::String("hello world".into()));
168 }
169
170 #[test]
171 fn parse_boolean_flag() {
172 let attrs = parse_attrs("[sortable]").unwrap();
173 assert_eq!(attrs["sortable"], AttrValue::Bool(true));
174 }
175
176 #[test]
177 fn parse_numeric() {
178 let attrs = parse_attrs("[count=42]").unwrap();
179 assert_eq!(attrs["count"], AttrValue::Number(42.0));
180 }
181
182 #[test]
183 fn parse_multiple() {
184 let attrs = parse_attrs(r#"[id=x sortable key="val"]"#).unwrap();
185 assert_eq!(attrs.len(), 3);
186 assert_eq!(attrs["id"], AttrValue::String("x".into()));
187 assert_eq!(attrs["sortable"], AttrValue::Bool(true));
188 assert_eq!(attrs["key"], AttrValue::String("val".into()));
189 }
190
191 #[test]
192 fn parse_escaped_quote() {
193 let attrs = parse_attrs(r#"[key="say \"hi\""]"#).unwrap();
194 assert_eq!(attrs["key"], AttrValue::String(r#"say "hi""#.into()));
195 }
196
197 #[test]
198 fn parse_no_brackets() {
199 let attrs = parse_attrs("key=value").unwrap();
200 assert_eq!(attrs.len(), 1);
201 assert_eq!(attrs["key"], AttrValue::String("value".into()));
202 }
203
204 #[test]
205 fn parse_bool_values() {
206 let attrs = parse_attrs("[enabled=true disabled=false]").unwrap();
207 assert_eq!(attrs["enabled"], AttrValue::Bool(true));
208 assert_eq!(attrs["disabled"], AttrValue::Bool(false));
209 }
210
211 #[test]
212 fn parse_null_value() {
213 let attrs = parse_attrs("[val=null]").unwrap();
214 assert_eq!(attrs["val"], AttrValue::Null);
215 }
216
217 #[test]
218 fn parse_float_number() {
219 let attrs = parse_attrs("[ratio=3.14]").unwrap();
220 assert_eq!(attrs["ratio"], AttrValue::Number(3.14));
221 }
222}