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