1use crate::shape::{CssShape, ShapePoint};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ShapeParseError {
15 UnknownFunction(alloc::string::String),
17 MissingParameter(alloc::string::String),
19 InvalidNumber(alloc::string::String),
21 InvalidSyntax(alloc::string::String),
23 EmptyInput,
25}
26
27impl core::fmt::Display for ShapeParseError {
28 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
29 match self {
30 ShapeParseError::UnknownFunction(func) => {
31 write!(f, "Unknown shape function: {}", func)
32 }
33 ShapeParseError::MissingParameter(param) => {
34 write!(f, "Missing required parameter: {}", param)
35 }
36 ShapeParseError::InvalidNumber(num) => {
37 write!(f, "Invalid numeric value: {}", num)
38 }
39 ShapeParseError::InvalidSyntax(msg) => {
40 write!(f, "Invalid syntax: {}", msg)
41 }
42 ShapeParseError::EmptyInput => {
43 write!(f, "Empty input")
44 }
45 }
46 }
47}
48
49pub fn parse_shape(input: &str) -> Result<CssShape, ShapeParseError> {
51 let input = input.trim();
52
53 if input.is_empty() {
54 return Err(ShapeParseError::EmptyInput);
55 }
56
57 let (func_name, args) = parse_function(input)?;
59
60 match func_name.as_str() {
61 "circle" => parse_circle(&args),
62 "ellipse" => parse_ellipse(&args),
63 "polygon" => parse_polygon(&args),
64 "inset" => parse_inset(&args),
65 "path" => parse_path(&args),
66 _ => Err(ShapeParseError::UnknownFunction(func_name)),
67 }
68}
69
70fn parse_function(
72 input: &str,
73) -> Result<(alloc::string::String, alloc::string::String), ShapeParseError> {
74 let open_paren = input
75 .find('(')
76 .ok_or_else(|| ShapeParseError::InvalidSyntax("Missing opening parenthesis".into()))?;
77
78 let close_paren = input
79 .rfind(')')
80 .ok_or_else(|| ShapeParseError::InvalidSyntax("Missing closing parenthesis".into()))?;
81
82 if close_paren <= open_paren {
83 return Err(ShapeParseError::InvalidSyntax("Invalid parentheses".into()));
84 }
85
86 let func_name = input[..open_paren].trim().to_string();
87 let args = input[open_paren + 1..close_paren].trim().to_string();
88
89 Ok((func_name, args))
90}
91
92fn parse_circle(args: &str) -> Result<CssShape, ShapeParseError> {
99 let parts: Vec<&str> = args.split_whitespace().collect();
100
101 if parts.is_empty() {
102 return Err(ShapeParseError::MissingParameter("radius".into()));
103 }
104
105 let radius = parse_length(parts[0])?;
106
107 let center = if parts.len() >= 4 && parts[1] == "at" {
108 let x = parse_length(parts[2])?;
109 let y = parse_length(parts[3])?;
110 ShapePoint::new(x, y)
111 } else {
112 ShapePoint::zero() };
114
115 Ok(CssShape::circle(center, radius))
116}
117
118fn parse_ellipse(args: &str) -> Result<CssShape, ShapeParseError> {
124 let parts: Vec<&str> = args.split_whitespace().collect();
125
126 if parts.len() < 2 {
127 return Err(ShapeParseError::MissingParameter(
128 "radius_x and radius_y".into(),
129 ));
130 }
131
132 let radius_x = parse_length(parts[0])?;
133 let radius_y = parse_length(parts[1])?;
134
135 let center = if parts.len() >= 5 && parts[2] == "at" {
136 let x = parse_length(parts[3])?;
137 let y = parse_length(parts[4])?;
138 ShapePoint::new(x, y)
139 } else {
140 ShapePoint::zero()
141 };
142
143 Ok(CssShape::ellipse(center, radius_x, radius_y))
144}
145
146fn parse_polygon(args: &str) -> Result<CssShape, ShapeParseError> {
153 let args = args.trim();
154
155 let point_str = if args.starts_with("nonzero,") || args.starts_with("evenodd,") {
157 let comma = args.find(',').unwrap();
159 &args[comma + 1..]
160 } else {
161 args
162 };
163
164 let pairs: Vec<&str> = point_str.split(',').map(|s| s.trim()).collect();
166
167 if pairs.is_empty() {
168 return Err(ShapeParseError::MissingParameter(
169 "at least one point".into(),
170 ));
171 }
172
173 let mut points = alloc::vec::Vec::new();
174
175 for pair in pairs {
176 let coords: Vec<&str> = pair.split_whitespace().collect();
177
178 if coords.len() < 2 {
179 return Err(ShapeParseError::InvalidSyntax(format!(
180 "Expected x y pair, got: {}",
181 pair
182 )));
183 }
184
185 let x = parse_length(coords[0])?;
186 let y = parse_length(coords[1])?;
187
188 points.push(ShapePoint::new(x, y));
189 }
190
191 if points.len() < 3 {
192 return Err(ShapeParseError::InvalidSyntax(
193 "Polygon must have at least 3 points".into(),
194 ));
195 }
196
197 Ok(CssShape::polygon(points.into()))
198}
199
200fn parse_inset(args: &str) -> Result<CssShape, ShapeParseError> {
209 let args = args.trim();
210
211 let (inset_str, border_radius) = if let Some(round_pos) = args.find("round") {
213 let insets = args[..round_pos].trim();
214 let radius_str = args[round_pos + 5..].trim();
215 let radius = parse_length(radius_str)?;
216 (insets, Some(radius))
217 } else {
218 (args, None)
219 };
220
221 let values: Vec<&str> = inset_str.split_whitespace().collect();
222
223 if values.is_empty() {
224 return Err(ShapeParseError::MissingParameter("inset values".into()));
225 }
226
227 let (top, right, bottom, left) = match values.len() {
229 1 => {
230 let all = parse_length(values[0])?;
231 (all, all, all, all)
232 }
233 2 => {
234 let vertical = parse_length(values[0])?;
235 let horizontal = parse_length(values[1])?;
236 (vertical, horizontal, vertical, horizontal)
237 }
238 3 => {
239 let top = parse_length(values[0])?;
240 let horizontal = parse_length(values[1])?;
241 let bottom = parse_length(values[2])?;
242 (top, horizontal, bottom, horizontal)
243 }
244 4 => {
245 let top = parse_length(values[0])?;
246 let right = parse_length(values[1])?;
247 let bottom = parse_length(values[2])?;
248 let left = parse_length(values[3])?;
249 (top, right, bottom, left)
250 }
251 _ => {
252 return Err(ShapeParseError::InvalidSyntax(
253 "Too many inset values (max 4)".into(),
254 ));
255 }
256 };
257
258 if let Some(radius) = border_radius {
259 Ok(CssShape::inset_rounded(top, right, bottom, left, radius))
260 } else {
261 Ok(CssShape::inset(top, right, bottom, left))
262 }
263}
264
265fn parse_path(args: &str) -> Result<CssShape, ShapeParseError> {
270 use crate::corety::AzString;
271
272 let args = args.trim();
273
274 if !args.starts_with('"') || !args.ends_with('"') {
276 return Err(ShapeParseError::InvalidSyntax(
277 "Path data must be quoted".into(),
278 ));
279 }
280
281 let path_data = AzString::from(&args[1..args.len() - 1]);
282
283 Ok(CssShape::Path(crate::shape::ShapePath { data: path_data }))
284}
285
286fn parse_length(s: &str) -> Result<f32, ShapeParseError> {
291 let s = s.trim();
292
293 if s.ends_with("px") {
294 let num_str = &s[..s.len() - 2];
295 num_str
296 .parse::<f32>()
297 .map_err(|_| ShapeParseError::InvalidNumber(s.to_string()))
298 } else if s.ends_with('%') {
299 let num_str = &s[..s.len() - 1];
300 let percent = num_str
301 .parse::<f32>()
302 .map_err(|_| ShapeParseError::InvalidNumber(s.to_string()))?;
303 Ok(percent)
306 } else {
307 s.parse::<f32>()
309 .map_err(|_| ShapeParseError::InvalidNumber(s.to_string()))
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use crate::{
317 corety::OptionF32,
318 shape::{ShapeCircle, ShapeEllipse, ShapeInset, ShapePath, ShapePolygon},
319 };
320
321 #[test]
322 fn test_parse_circle() {
323 let shape = parse_shape("circle(50px at 100px 100px)").unwrap();
324 match shape {
325 CssShape::Circle(ShapeCircle { center, radius }) => {
326 assert_eq!(radius, 50.0);
327 assert_eq!(center.x, 100.0);
328 assert_eq!(center.y, 100.0);
329 }
330 _ => panic!("Expected Circle"),
331 }
332 }
333
334 #[test]
335 fn test_parse_circle_no_position() {
336 let shape = parse_shape("circle(50px)").unwrap();
337 match shape {
338 CssShape::Circle(ShapeCircle { center, radius }) => {
339 assert_eq!(radius, 50.0);
340 assert_eq!(center.x, 0.0);
341 assert_eq!(center.y, 0.0);
342 }
343 _ => panic!("Expected Circle"),
344 }
345 }
346
347 #[test]
348 fn test_parse_ellipse() {
349 let shape = parse_shape("ellipse(50px 75px at 100px 100px)").unwrap();
350 match shape {
351 CssShape::Ellipse(ShapeEllipse {
352 center,
353 radius_x,
354 radius_y,
355 }) => {
356 assert_eq!(radius_x, 50.0);
357 assert_eq!(radius_y, 75.0);
358 assert_eq!(center.x, 100.0);
359 assert_eq!(center.y, 100.0);
360 }
361 _ => panic!("Expected Ellipse"),
362 }
363 }
364
365 #[test]
366 fn test_parse_polygon_rectangle() {
367 let shape = parse_shape("polygon(0px 0px, 100px 0px, 100px 100px, 0px 100px)").unwrap();
368 match shape {
369 CssShape::Polygon(ShapePolygon { points }) => {
370 assert_eq!(points.as_ref().len(), 4);
371 assert_eq!(points.as_ref()[0].x, 0.0);
372 assert_eq!(points.as_ref()[0].y, 0.0);
373 assert_eq!(points.as_ref()[2].x, 100.0);
374 assert_eq!(points.as_ref()[2].y, 100.0);
375 }
376 _ => panic!("Expected Polygon"),
377 }
378 }
379
380 #[test]
381 fn test_parse_polygon_star() {
382 let shape = parse_shape(
384 "polygon(50px 0px, 61px 35px, 98px 35px, 68px 57px, 79px 91px, 50px 70px, 21px 91px, \
385 32px 57px, 2px 35px, 39px 35px)",
386 )
387 .unwrap();
388 match shape {
389 CssShape::Polygon(ShapePolygon { points }) => {
390 assert_eq!(points.as_ref().len(), 10); }
392 _ => panic!("Expected Polygon"),
393 }
394 }
395
396 #[test]
397 fn test_parse_inset() {
398 let shape = parse_shape("inset(10px 20px 30px 40px)").unwrap();
399 match shape {
400 CssShape::Inset(ShapeInset {
401 inset_top,
402 inset_right,
403 inset_bottom,
404 inset_left,
405 border_radius,
406 }) => {
407 assert_eq!(inset_top, 10.0);
408 assert_eq!(inset_right, 20.0);
409 assert_eq!(inset_bottom, 30.0);
410 assert_eq!(inset_left, 40.0);
411 assert!(matches!(border_radius, OptionF32::None));
412 }
413 _ => panic!("Expected Inset"),
414 }
415 }
416
417 #[test]
418 fn test_parse_inset_rounded() {
419 let shape = parse_shape("inset(10px round 5px)").unwrap();
420 match shape {
421 CssShape::Inset(ShapeInset {
422 inset_top,
423 inset_right,
424 inset_bottom,
425 inset_left,
426 border_radius,
427 }) => {
428 assert_eq!(inset_top, 10.0);
429 assert_eq!(inset_right, 10.0);
430 assert_eq!(inset_bottom, 10.0);
431 assert_eq!(inset_left, 10.0);
432 assert!(matches!(border_radius, OptionF32::Some(r) if r == 5.0));
433 }
434 _ => panic!("Expected Inset"),
435 }
436 }
437
438 #[test]
439 fn test_parse_path() {
440 let shape = parse_shape(r#"path("M 0 0 L 100 0 L 100 100 Z")"#).unwrap();
441 match shape {
442 CssShape::Path(ShapePath { data }) => {
443 assert_eq!(data.as_str(), "M 0 0 L 100 0 L 100 100 Z");
444 }
445 _ => panic!("Expected Path"),
446 }
447 }
448
449 #[test]
450 fn test_invalid_function() {
451 let result = parse_shape("unknown(50px)");
452 assert!(result.is_err());
453 }
454
455 #[test]
456 fn test_empty_input() {
457 let result = parse_shape("");
458 assert!(matches!(result, Err(ShapeParseError::EmptyInput)));
459 }
460}