1use crate::layout_ir::{Edges, LengthSpec, ParseError, Rgba};
9
10pub fn parse_length(ctx: &str, name: &str, value: &str) -> Result<LengthSpec, ParseError> {
12 let v = value.trim();
13 if v == "auto" {
14 return Ok(LengthSpec::Auto);
15 }
16 if let Some(rest) = v.strip_suffix('%') {
17 return Ok(LengthSpec::Percent(parse_num(ctx, name, value, rest)?));
18 }
19 if let Some(rest) = v.strip_suffix("px") {
20 return Ok(LengthSpec::Px(parse_num(ctx, name, value, rest)?));
21 }
22 Ok(LengthSpec::Px(parse_num(ctx, name, value, v)?))
23}
24
25pub fn parse_color(ctx: &str, name: &str, value: &str) -> Result<Rgba, ParseError> {
27 let v = value.trim();
28 let body = v
29 .strip_prefix('#')
30 .ok_or_else(|| bad(ctx, name, value, "expected '#rrggbb' or '#rrggbbaa'".into()))?;
31 if body.len() != 6 && body.len() != 8 {
32 return Err(bad(
33 ctx,
34 name,
35 value,
36 "expected '#rrggbb' or '#rrggbbaa'".into(),
37 ));
38 }
39 let pair = |i: usize| -> Result<u8, ParseError> {
40 u8::from_str_radix(&body[i..i + 2], 16)
41 .map_err(|e| bad(ctx, name, value, format!("bad hex: {e}")))
42 };
43 let r = pair(0)? as f32 / 255.0;
44 let g = pair(2)? as f32 / 255.0;
45 let b = pair(4)? as f32 / 255.0;
46 let a = if body.len() == 8 {
47 pair(6)? as f32 / 255.0
48 } else {
49 1.0
50 };
51 Ok(Rgba { r, g, b, a })
52}
53
54pub fn parse_bg(
60 ctx: &str,
61 name: &str,
62 value: &str,
63) -> Result<crate::layout_ir::BgSpec, ParseError> {
64 use crate::layout_ir::BgSpec;
65 let trimmed = value.trim();
66 if let Some(path) = parse_url(ctx, name, value)? {
67 return Ok(BgSpec::Image(path));
68 }
69 if let Some(inner) = trimmed
70 .strip_prefix("linear-gradient(")
71 .and_then(|s| s.strip_suffix(')'))
72 {
73 return parse_linear_gradient(ctx, name, value, inner).map(|(angle, stops)| {
74 BgSpec::Linear {
75 angle_deg: angle,
76 stops,
77 }
78 });
79 }
80 if let Some(inner) = trimmed
81 .strip_prefix("radial-gradient(")
82 .and_then(|s| s.strip_suffix(')'))
83 {
84 return parse_radial_gradient(ctx, name, value, inner)
85 .map(|(radius, stops)| BgSpec::Radial { radius, stops });
86 }
87 if let Some(inner) = trimmed
88 .strip_prefix("conic-gradient(")
89 .and_then(|s| s.strip_suffix(')'))
90 {
91 return parse_conic_gradient(ctx, name, value, inner)
92 .map(|(from_deg, stops)| BgSpec::Conic { from_deg, stops });
93 }
94 Ok(BgSpec::Solid(parse_color(ctx, name, value)?))
95}
96
97pub fn parse_url(ctx: &str, name: &str, value: &str) -> Result<Option<String>, ParseError> {
101 let trimmed = value.trim();
102 let Some(inner) = trimmed
103 .get(..4)
104 .filter(|head| head.eq_ignore_ascii_case("url("))
105 .and_then(|_| trimmed[4..].strip_suffix(')'))
106 else {
107 return Ok(None);
108 };
109 let inner = inner.trim();
110 let path = ['"', '\'']
111 .iter()
112 .find_map(|q| {
113 inner
114 .strip_prefix(*q)
115 .and_then(|rest| rest.strip_suffix(*q))
116 })
117 .unwrap_or(inner);
118 if path.is_empty() {
119 return Err(bad(ctx, name, value, "url() names no file".into()));
120 }
121 Ok(Some(path.to_string()))
122}
123
124pub fn parse_image_fit(
128 ctx: &str,
129 name: &str,
130 value: &str,
131) -> Result<crate::layout_ir::ImageFitSpec, ParseError> {
132 use crate::layout_ir::ImageFitSpec;
133 Ok(match value.trim() {
134 "fill" => ImageFitSpec::Fill,
135 "cover" => ImageFitSpec::Cover,
136 "contain" => ImageFitSpec::Contain,
137 "none" => ImageFitSpec::None,
138 "scale-down" => ImageFitSpec::ScaleDown,
139 other => {
140 return Err(bad(
141 ctx,
142 name,
143 value,
144 format!(
145 "unknown fit '{other}' (supported: fill, cover, contain, none, scale-down)"
146 ),
147 ));
148 }
149 })
150}
151
152fn parse_radial_gradient(
155 ctx: &str,
156 name: &str,
157 full_value: &str,
158 inner: &str,
159) -> Result<(f32, Vec<(f32, crate::layout_ir::Rgba)>), ParseError> {
160 let parts: Vec<&str> = inner
161 .split(',')
162 .map(|p| p.trim())
163 .filter(|p| !p.is_empty())
164 .collect();
165 if parts.len() < 2 {
166 return Err(bad(
167 ctx,
168 name,
169 full_value,
170 "radial-gradient needs at least two color stops".into(),
171 ));
172 }
173 let n = parts.len();
174 let mut stops: Vec<(f32, crate::layout_ir::Rgba)> = Vec::with_capacity(n);
175 for (i, raw) in parts.iter().enumerate() {
176 let mut bits = raw.split_whitespace();
177 let color_str = bits
178 .next()
179 .ok_or_else(|| bad(ctx, name, full_value, format!("empty stop #{}", i + 1)))?;
180 let color = parse_color(ctx, name, color_str)?;
181 let offset = match bits.next() {
182 Some(s) => parse_offset(ctx, name, full_value, s)?,
183 None => i as f32 / (n - 1).max(1) as f32,
184 };
185 stops.push((offset, color));
186 }
187 stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
188 Ok((1.0, stops))
189}
190
191fn parse_conic_gradient(
193 ctx: &str,
194 name: &str,
195 full_value: &str,
196 inner: &str,
197) -> Result<(f32, Vec<(f32, crate::layout_ir::Rgba)>), ParseError> {
198 let parts: Vec<&str> = inner
199 .split(',')
200 .map(|p| p.trim())
201 .filter(|p| !p.is_empty())
202 .collect();
203 if parts.is_empty() {
204 return Err(bad(
205 ctx,
206 name,
207 full_value,
208 "conic-gradient needs at least one stop".into(),
209 ));
210 }
211 let (from_deg, stop_parts): (f32, &[&str]) = if let Some(rest) = parts[0].strip_prefix("from ")
212 {
213 let num = rest.trim().strip_suffix("deg").unwrap_or(rest.trim());
214 let v: f32 = num.trim().parse().map_err(|e| {
215 bad(
216 ctx,
217 name,
218 full_value,
219 format!("bad angle '{}': {e}", parts[0]),
220 )
221 })?;
222 (v, &parts[1..])
223 } else {
224 (0.0, &parts[..])
225 };
226 if stop_parts.len() < 2 {
227 return Err(bad(
228 ctx,
229 name,
230 full_value,
231 "conic-gradient needs at least two color stops".into(),
232 ));
233 }
234 let n = stop_parts.len();
235 let mut stops: Vec<(f32, crate::layout_ir::Rgba)> = Vec::with_capacity(n);
236 for (i, raw) in stop_parts.iter().enumerate() {
237 let mut bits = raw.split_whitespace();
238 let color_str = bits
239 .next()
240 .ok_or_else(|| bad(ctx, name, full_value, format!("empty stop #{}", i + 1)))?;
241 let color = parse_color(ctx, name, color_str)?;
242 let offset = match bits.next() {
243 Some(s) => parse_offset(ctx, name, full_value, s)?,
244 None => i as f32 / (n - 1).max(1) as f32,
245 };
246 stops.push((offset, color));
247 }
248 stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
249 Ok((from_deg, stops))
250}
251
252fn parse_linear_gradient(
253 ctx: &str,
254 name: &str,
255 full_value: &str,
256 inner: &str,
257) -> Result<(f32, Vec<(f32, crate::layout_ir::Rgba)>), ParseError> {
258 let parts: Vec<&str> = inner
259 .split(',')
260 .map(|p| p.trim())
261 .filter(|p| !p.is_empty())
262 .collect();
263 if parts.len() < 2 {
264 return Err(bad(
265 ctx,
266 name,
267 full_value,
268 "linear-gradient needs at least one direction and one stop".into(),
269 ));
270 }
271 let (angle_deg, stop_parts): (f32, &[&str]) = match parts[0].strip_suffix("deg") {
272 Some(num) => (
273 num.trim().parse::<f32>().map_err(|e| {
274 bad(
275 ctx,
276 name,
277 full_value,
278 format!("bad angle '{}': {e}", parts[0]),
279 )
280 })?,
281 &parts[1..],
282 ),
283 None => (180.0, &parts[..]), };
285 if stop_parts.len() < 2 {
286 return Err(bad(
287 ctx,
288 name,
289 full_value,
290 "linear-gradient needs at least two color stops".into(),
291 ));
292 }
293 let n = stop_parts.len();
294 let mut stops: Vec<(f32, crate::layout_ir::Rgba)> = Vec::with_capacity(n);
295 for (i, raw) in stop_parts.iter().enumerate() {
296 let mut bits = raw.split_whitespace();
297 let color_str = bits
298 .next()
299 .ok_or_else(|| bad(ctx, name, full_value, format!("empty stop #{}", i + 1)))?;
300 let color = parse_color(ctx, name, color_str)?;
301 let offset = match bits.next() {
302 Some(s) => parse_offset(ctx, name, full_value, s)?,
303 None => i as f32 / (n - 1).max(1) as f32,
304 };
305 stops.push((offset, color));
306 }
307 stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
308 Ok((angle_deg, stops))
309}
310
311fn parse_offset(ctx: &str, name: &str, full_value: &str, raw: &str) -> Result<f32, ParseError> {
312 if let Some(num) = raw.strip_suffix('%') {
313 let n: f32 = num
314 .trim()
315 .parse()
316 .map_err(|e: std::num::ParseFloatError| bad(ctx, name, full_value, e.to_string()))?;
317 Ok(n / 100.0)
318 } else {
319 raw.trim().parse::<f32>().map_err(|e| {
320 bad(
321 ctx,
322 name,
323 full_value,
324 format!("bad stop offset '{raw}': {e}"),
325 )
326 })
327 }
328}
329
330fn parse_edge_term(
334 ctx: &str,
335 name: &str,
336 value: &str,
337 s: &str,
338) -> Result<(f32, Option<f32>), ParseError> {
339 let s = s.trim();
340 if let Some(rest) = s.strip_suffix('%') {
341 let p: f32 = rest
342 .trim()
343 .parse()
344 .map_err(|e: std::num::ParseFloatError| {
345 bad(ctx, name, value, format!("bad percent '{s}': {e}"))
346 })?;
347 return Ok((0.0, Some(p)));
348 }
349 Ok((parse_num(ctx, name, value, s)?, None))
350}
351
352pub fn parse_edges(ctx: &str, name: &str, value: &str) -> Result<Edges, ParseError> {
362 edges_with(
363 value,
364 |s| parse_edge_term(ctx, name, value, s),
365 |other| {
366 bad(
367 ctx,
368 name,
369 value,
370 format!("expected 1, 2, 3, or 4 numbers, got {other}"),
371 )
372 },
373 )
374}
375
376pub fn parse_inset_edges(ctx: &str, name: &str, value: &str) -> Result<Edges, ParseError> {
379 edges_with(
380 value,
381 |s| {
382 if s.trim() == "auto" {
383 Ok((f32::NAN, None))
384 } else {
385 parse_edge_term(ctx, name, value, s)
386 }
387 },
388 |other| {
389 bad(
390 ctx,
391 name,
392 value,
393 format!("expected 1, 2, 3, or 4 numbers or `auto`, got {other}"),
394 )
395 },
396 )
397}
398
399pub fn parse_inset_term(ctx: &str, name: &str, value: &str) -> Result<f32, ParseError> {
401 if value.trim() == "auto" {
402 return Ok(f32::NAN);
403 }
404 parse_num(ctx, name, value, value.trim())
405}
406
407fn edges_with(
410 value: &str,
411 term: impl Fn(&str) -> Result<(f32, Option<f32>), ParseError>,
412 arity: impl Fn(usize) -> ParseError,
413) -> Result<Edges, ParseError> {
414 let parts: Vec<&str> = value.split_whitespace().collect();
415 let n = |s: &str| term(s);
416 let (t, r, b, l) = match parts.len() {
417 1 => {
418 let v = n(parts[0])?;
419 (v, v, v, v)
420 }
421 2 => {
422 let v = n(parts[0])?;
423 let h = n(parts[1])?;
424 (v, h, v, h)
425 }
426 3 => {
427 let t = n(parts[0])?;
428 let h = n(parts[1])?;
429 let b = n(parts[2])?;
430 (t, h, b, h)
431 }
432 4 => (n(parts[0])?, n(parts[1])?, n(parts[2])?, n(parts[3])?),
433 other => return Err(arity(other)),
434 };
435 Ok(Edges {
436 top: t.0,
437 right: r.0,
438 bottom: b.0,
439 left: l.0,
440 pct_top: t.1,
441 pct_right: r.1,
442 pct_bottom: b.1,
443 pct_left: l.1,
444 ..Edges::default()
445 })
446}
447
448pub fn parse_border_width_term(
452 ctx: &str,
453 name: &str,
454 value: &str,
455 s: &str,
456) -> Result<f32, ParseError> {
457 match s.trim() {
458 "thin" => Ok(1.0),
459 "medium" => Ok(3.0),
460 "thick" => Ok(5.0),
461 t if t.ends_with('%') => Err(bad(
462 ctx,
463 name,
464 value,
465 "border widths cannot be percentages".into(),
466 )),
467 t => parse_num(ctx, name, value, t),
468 }
469}
470
471pub fn parse_border_width_edges(ctx: &str, name: &str, value: &str) -> Result<Edges, ParseError> {
474 let parts: Vec<&str> = value.split_whitespace().collect();
475 let n = |s: &str| parse_border_width_term(ctx, name, value, s);
476 match parts.len() {
477 1 => Ok(Edges::all(n(parts[0])?)),
478 2 => {
479 let v = n(parts[0])?;
480 let h = n(parts[1])?;
481 Ok(Edges {
482 top: v,
483 right: h,
484 bottom: v,
485 left: h,
486 ..Edges::default()
487 })
488 }
489 3 => {
490 let t = n(parts[0])?;
491 let h = n(parts[1])?;
492 let b = n(parts[2])?;
493 Ok(Edges {
494 top: t,
495 right: h,
496 bottom: b,
497 left: h,
498 ..Edges::default()
499 })
500 }
501 4 => Ok(Edges {
502 top: n(parts[0])?,
503 right: n(parts[1])?,
504 bottom: n(parts[2])?,
505 left: n(parts[3])?,
506 ..Edges::default()
507 }),
508 other => Err(bad(
509 ctx,
510 name,
511 value,
512 format!("expected 1, 2, 3, or 4 border widths, got {other}"),
513 )),
514 }
515}
516
517pub fn parse_border_style(
521 ctx: &str,
522 name: &str,
523 value: &str,
524) -> Result<crate::layout_ir::BorderStyleSpec, ParseError> {
525 use crate::layout_ir::BorderStyleSpec;
526 match value.trim() {
527 "none" | "hidden" => Ok(BorderStyleSpec::None),
528 "solid" => Ok(BorderStyleSpec::Solid),
529 other => Err(bad(
530 ctx,
531 name,
532 value,
533 format!("unsupported border-style '{other}' (supported: none, solid)"),
534 )),
535 }
536}
537
538pub struct BorderShorthand {
540 pub width: Option<f32>,
542 pub style: Option<crate::layout_ir::BorderStyleSpec>,
544 pub color: Option<crate::layout_ir::Rgba>,
546}
547
548pub fn parse_border_shorthand(
556 ctx: &str,
557 name: &str,
558 value: &str,
559) -> Result<BorderShorthand, ParseError> {
560 use crate::layout_ir::BorderStyleSpec;
561 let mut out = BorderShorthand {
562 width: None,
563 style: None,
564 color: None,
565 };
566 for tok in value.split_whitespace() {
567 match tok {
568 "none" | "hidden" => out.style = Some(BorderStyleSpec::None),
569 "solid" => out.style = Some(BorderStyleSpec::Solid),
570 "dashed" | "dotted" | "double" | "groove" | "ridge" | "inset" | "outset" => {
571 return Err(bad(
572 ctx,
573 name,
574 value,
575 format!("unsupported border-style '{tok}' (supported: none, solid)"),
576 ));
577 }
578 t if t.starts_with('#') => out.color = Some(parse_color(ctx, name, t)?),
579 t => out.width = Some(parse_border_width_term(ctx, name, value, t)?),
580 }
581 }
582 if out.style.is_none() && (out.width.is_some() || out.color.is_some()) {
583 out.style = Some(BorderStyleSpec::Solid);
584 }
585 if out.style.is_none() {
586 return Err(bad(
587 ctx,
588 name,
589 value,
590 "expected '<width> [solid|none] <#color>' (any order) or 'none'".into(),
591 ));
592 }
593 Ok(out)
594}
595
596pub fn parse_font_weight(ctx: &str, name: &str, value: &str) -> Result<u16, ParseError> {
600 match value.trim() {
601 "normal" => Ok(400),
602 "bold" => Ok(700),
603 "lighter" | "bolder" => Err(bad(
604 ctx,
605 name,
606 value,
607 "relative weights (lighter/bolder) are not supported - use a number 1..=1000".into(),
608 )),
609 v => {
610 let n = parse_num(ctx, name, value, v)?;
611 if !(1.0..=1000.0).contains(&n) {
612 return Err(bad(ctx, name, value, "font-weight must be 1..=1000".into()));
613 }
614 Ok(n.round() as u16)
615 }
616 }
617}
618
619pub fn parse_corner_radii(ctx: &str, name: &str, value: &str) -> Result<[f32; 4], ParseError> {
627 let parts: Vec<&str> = value.split_whitespace().collect();
628 let n = |s: &str| parse_num(ctx, name, value, s);
629 Ok(match parts.as_slice() {
630 [a] => {
631 let a = n(a)?;
632 [a; 4]
633 }
634 [a, b] => {
635 let (a, b) = (n(a)?, n(b)?);
636 [a, b, a, b]
637 }
638 [a, b, c] => {
639 let (a, b, c) = (n(a)?, n(b)?, n(c)?);
640 [a, b, c, b]
641 }
642 [a, b, c, d] => [n(a)?, n(b)?, n(c)?, n(d)?],
643 other => {
644 return Err(bad(
645 ctx,
646 name,
647 value,
648 format!("expected 1, 2, 3, or 4 radii, got {}", other.len()),
649 ));
650 }
651 })
652}
653
654pub fn parse_f32(ctx: &str, name: &str, value: &str) -> Result<f32, ParseError> {
656 parse_num(ctx, name, value, value.trim())
657}
658
659pub fn parse_i32(ctx: &str, name: &str, value: &str) -> Result<i32, ParseError> {
661 value
662 .trim()
663 .parse()
664 .map_err(|e: std::num::ParseIntError| bad(ctx, name, value, e.to_string()))
665}
666
667fn parse_num(ctx: &str, name: &str, value: &str, s: &str) -> Result<f32, ParseError> {
668 let s = s.trim();
669 let s = s.strip_suffix("px").map(str::trim_end).unwrap_or(s);
672 s.parse().map_err(|e: std::num::ParseFloatError| {
673 bad(ctx, name, value, format!("bad number '{s}': {e}"))
674 })
675}
676
677pub fn bad(ctx: &str, name: &str, value: &str, reason: String) -> ParseError {
679 ParseError::BadAttribute {
680 name: name.to_string(),
681 value: value.to_string(),
682 tag: ctx.to_string(),
683 reason,
684 }
685}