1use super::*;
4
5pub(crate) fn parse_border_shorthand(s: &str) -> (Option<String>, Option<String>, Option<String>) {
6 let mut width = None;
7 let mut style = None;
8 let mut color = None;
9
10 for token in s.split_whitespace() {
11 let t = token.trim();
12 if t.is_empty() {
13 continue;
14 }
15 let t_lower = t.to_lowercase();
16
17 if matches!(
23 t_lower.as_str(),
24 "none" | "solid" | "dashed" | "dotted" | "double"
25 ) {
26 style = Some(t_lower);
27 continue;
28 }
29 if t_lower == "hidden" {
30 style = Some(String::from("none"));
31 continue;
32 }
33 if matches!(t_lower.as_str(), "groove" | "ridge" | "inset" | "outset") {
34 style = Some(String::from("solid"));
35 continue;
36 }
37
38 if t_lower.ends_with("px")
40 || t_lower.parse::<f32>().is_ok()
41 || t_lower == "thin"
42 || t_lower == "medium"
43 || t_lower == "thick"
44 {
45 let val = match t_lower.as_str() {
46 "thin" => String::from("1px"),
47 "medium" => String::from("3px"),
48 "thick" => String::from("5px"),
49 _ => t_lower,
50 };
51 width = Some(val);
52 continue;
53 }
54
55 if t_lower.starts_with('#') || t_lower.starts_with("rgb") || parse_color(&t_lower).is_some()
57 {
58 color = Some(t_lower);
59 continue;
60 }
61 }
62
63 (width, style, color)
64}
65
66pub(crate) fn strip_css_wide_keywords(specified_values: &mut BTreeMap<String, String>) {
77 let keys_to_remove: alloc::vec::Vec<String> = specified_values
78 .iter()
79 .filter(|(_, v)| {
80 matches!(
81 v.trim().to_ascii_lowercase().as_str(),
82 "initial" | "inherit" | "unset" | "revert"
83 )
84 })
85 .map(|(k, _)| k.clone())
86 .collect();
87 for k in keys_to_remove {
88 specified_values.remove(&k);
89 }
90}
91
92pub(crate) fn resolve_all_shorthand(specified_values: &mut BTreeMap<String, String>) {
100 let is_reset = specified_values
101 .get("all")
102 .map(|v| {
103 matches!(
104 v.trim().to_ascii_lowercase().as_str(),
105 "unset" | "initial" | "revert"
106 )
107 })
108 .unwrap_or(false);
109 if !is_reset {
110 return;
111 }
112 let keys_to_remove: alloc::vec::Vec<String> = specified_values
113 .keys()
114 .filter(|k| *k != "all" && !k.starts_with("--"))
115 .cloned()
116 .collect();
117 for k in keys_to_remove {
118 specified_values.remove(&k);
119 }
120 specified_values.remove("all");
121}
122
123pub(crate) fn expand_logical_box_shorthands(specified_values: &mut BTreeMap<String, String>) {
129 for prefix in ["margin", "padding", "inset"] {
130 for axis in ["block", "inline"] {
131 let shorthand = alloc::format!("{prefix}-{axis}");
132 let start_prop = alloc::format!("{prefix}-{axis}-start");
133 let end_prop = alloc::format!("{prefix}-{axis}-end");
134 if let Some(v) = specified_values.get(&shorthand).cloned() {
135 let toks: Vec<&str> = v.split_whitespace().collect();
136 if let Some(&start) = toks.first() {
137 let end = toks.get(1).copied().unwrap_or(start);
138 specified_values.entry(start_prop).or_insert_with(|| String::from(start));
139 specified_values.entry(end_prop).or_insert_with(|| String::from(end));
140 }
141 }
142 }
143 }
144}
145
146pub(crate) fn resolve_logical_properties(specified_values: &mut BTreeMap<String, String>) {
153 let rtl = specified_values
154 .get("direction")
155 .map(|v| v.trim().eq_ignore_ascii_case("rtl"))
156 .unwrap_or(false);
157 let (inline_start, inline_end) = if rtl {
158 ("right", "left")
159 } else {
160 ("left", "right")
161 };
162 for prefix in ["margin", "padding", "border"] {
164 let pairs = [
165 (
166 alloc::format!("{prefix}-block-start"),
167 alloc::format!("{prefix}-top"),
168 ),
169 (
170 alloc::format!("{prefix}-block-end"),
171 alloc::format!("{prefix}-bottom"),
172 ),
173 (
174 alloc::format!("{prefix}-inline-start"),
175 alloc::format!("{prefix}-{inline_start}"),
176 ),
177 (
178 alloc::format!("{prefix}-inline-end"),
179 alloc::format!("{prefix}-{inline_end}"),
180 ),
181 ];
182 for (logical, physical) in pairs {
183 if specified_values.contains_key(&physical) {
184 continue;
185 }
186 if let Some(v) = specified_values.get(&logical).cloned() {
187 specified_values.insert(physical, v);
188 }
189 }
190 }
191 for suffix in ["width", "style", "color"] {
194 let pairs = [
195 (
196 alloc::format!("border-block-start-{suffix}"),
197 alloc::format!("border-top-{suffix}"),
198 ),
199 (
200 alloc::format!("border-block-end-{suffix}"),
201 alloc::format!("border-bottom-{suffix}"),
202 ),
203 (
204 alloc::format!("border-inline-start-{suffix}"),
205 alloc::format!("border-{inline_start}-{suffix}"),
206 ),
207 (
208 alloc::format!("border-inline-end-{suffix}"),
209 alloc::format!("border-{inline_end}-{suffix}"),
210 ),
211 ];
212 for (logical, physical) in pairs {
213 if specified_values.contains_key(&physical) {
214 continue;
215 }
216 if let Some(v) = specified_values.get(&logical).cloned() {
217 specified_values.insert(physical, v);
218 }
219 }
220 }
221 let inset_pairs = [
223 (String::from("inset-block-start"), String::from("top")),
224 (String::from("inset-block-end"), String::from("bottom")),
225 (
226 String::from("inset-inline-start"),
227 inline_start.to_string(),
228 ),
229 (
230 String::from("inset-inline-end"),
231 inline_end.to_string(),
232 ),
233 ];
234 for (logical, physical) in inset_pairs {
235 if specified_values.contains_key(&physical) {
236 continue;
237 }
238 if let Some(v) = specified_values.get(&logical).cloned() {
239 specified_values.insert(physical, v);
240 }
241 }
242}
243
244pub(crate) fn replace_ignore_case(haystack: &str, from: &str, to: &str) -> String {
247 if from.is_empty() {
248 return String::from(haystack);
249 }
250 let hay_lower = haystack.to_ascii_lowercase();
251 let from_lower = from.to_ascii_lowercase();
252 let mut result = String::new();
253 let mut rest = haystack;
254 let mut rest_lower: &str = &hay_lower;
255 while let Some(pos) = rest_lower.find(from_lower.as_str()) {
256 result.push_str(rest.get(..pos).unwrap_or(""));
257 result.push_str(to);
258 rest = rest.get(pos + from.len()..).unwrap_or("");
259 rest_lower = rest_lower.get(pos + from.len()..).unwrap_or("");
260 }
261 result.push_str(rest);
262 result
263}
264
265pub(crate) fn resolve_current_color(specified_values: &mut BTreeMap<String, String>) {
269 let Some(color_val) = specified_values.get("color").cloned() else {
270 return;
271 };
272 let keys: alloc::vec::Vec<String> = specified_values
273 .iter()
274 .filter(|(k, v)| k.as_str() != "color" && v.to_ascii_lowercase().contains("currentcolor"))
275 .map(|(k, _)| k.clone())
276 .collect();
277 for k in keys {
278 if let Some(v) = specified_values.get(&k) {
279 let replaced = replace_ignore_case(v, "currentcolor", &color_val);
280 specified_values.insert(k, replaced);
281 }
282 }
283}
284
285pub(crate) fn expand_place_shorthands(specified_values: &mut BTreeMap<String, String>) {
290 for prefix in ["items", "content", "self"] {
291 let shorthand_key = alloc::format!("place-{prefix}");
292 let Some(v) = specified_values.get(&shorthand_key).cloned() else {
293 continue;
294 };
295 let toks: alloc::vec::Vec<&str> = v.split_whitespace().collect();
296 let (align_v, justify_v) = match toks.as_slice() {
297 [a] => (*a, *a),
298 [a, j] => (*a, *j),
299 _ => continue,
300 };
301 specified_values
302 .entry(alloc::format!("align-{prefix}"))
303 .or_insert_with(|| String::from(align_v));
304 specified_values
305 .entry(alloc::format!("justify-{prefix}"))
306 .or_insert_with(|| String::from(justify_v));
307 }
308}
309
310pub(crate) fn strip_quoted_segments(s: &str) -> String {
314 let mut out = String::new();
315 let mut chars = s.chars().peekable();
316 while let Some(c) = chars.next() {
317 if c == '"' || c == '\'' {
318 let quote = c;
319 for c2 in chars.by_ref() {
320 if c2 == quote {
321 break;
322 }
323 }
324 } else {
325 out.push(c);
326 }
327 }
328 out.split_whitespace().collect::<alloc::vec::Vec<_>>().join(" ")
329}
330
331pub(crate) fn expand_grid_template_shorthand(specified_values: &mut BTreeMap<String, String>) {
338 let Some(v) = specified_values.get("grid-template").cloned() else {
339 return;
340 };
341 let (rows_part, cols_part) = match v.split_once('/') {
342 Some((r, c)) => (r.trim(), Some(c.trim())),
343 None => (v.trim(), None),
344 };
345 if rows_part.contains('"') || rows_part.contains('\'') {
346 specified_values
348 .entry(String::from("grid-template-areas"))
349 .or_insert_with(|| String::from(rows_part));
350 let tracks = strip_quoted_segments(rows_part);
352 if !tracks.is_empty() {
353 specified_values
354 .entry(String::from("grid-template-rows"))
355 .or_insert(tracks);
356 }
357 } else if !rows_part.is_empty() {
358 specified_values
359 .entry(String::from("grid-template-rows"))
360 .or_insert_with(|| String::from(rows_part));
361 }
362 if let Some(cols) = cols_part {
363 if !cols.is_empty() {
364 specified_values
365 .entry(String::from("grid-template-columns"))
366 .or_insert_with(|| String::from(cols));
367 }
368 }
369}
370
371pub(crate) fn expand_text_emphasis_shorthand(specified_values: &mut BTreeMap<String, String>) {
375 let Some(v) = specified_values.get("text-emphasis").cloned() else {
376 return;
377 };
378 let mut style_tok: Option<&str> = None;
379 let mut color_tok: Option<&str> = None;
380 for tok in v.split_whitespace() {
381 if matches!(tok, "filled" | "open") {
382 continue;
383 }
384 if matches!(tok, "none" | "dot" | "circle" | "double-circle" | "triangle" | "sesame") {
385 style_tok = Some(tok);
386 } else if parse_color(tok).is_some() {
387 color_tok = Some(tok);
388 } else if style_tok.is_none() {
389 style_tok = Some(tok);
391 }
392 }
393 if let Some(s) = style_tok {
394 specified_values
395 .entry(String::from("text-emphasis-style"))
396 .or_insert_with(|| String::from(s));
397 }
398 if let Some(c) = color_tok {
399 specified_values
400 .entry(String::from("text-emphasis-color"))
401 .or_insert_with(|| String::from(c));
402 }
403}
404
405pub(crate) fn expand_font_shorthand(specified_values: &mut BTreeMap<String, String>) {
413 let Some(font) = specified_values.get("font").cloned() else {
414 return;
415 };
416 let trimmed = font.trim();
417 let lower_whole = trimmed.to_ascii_lowercase();
418 if matches!(
419 lower_whole.as_str(),
420 "caption" | "icon" | "menu" | "message-box" | "small-caption" | "status-bar"
421 ) {
422 return;
423 }
424
425 fn is_size_token(t: &str) -> bool {
428 let low = t.to_ascii_lowercase();
429 const SIZE_KEYWORDS: &[&str] = &[
430 "xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "larger",
431 "smaller",
432 ];
433 if SIZE_KEYWORDS.contains(&low.as_str()) {
434 return true;
435 }
436 let first_part = low.split('/').next().unwrap_or(&low);
437 let starts_digit = first_part
438 .chars()
439 .next()
440 .map(|c| c.is_ascii_digit())
441 .unwrap_or(false);
442 if !starts_digit {
443 return false;
444 }
445 const SIZE_UNITS: &[&str] = &[
450 "px", "em", "rem", "pt", "pc", "in", "cm", "mm", "q", "%", "vh", "vw", "vmin", "vmax",
451 "ex", "ch",
452 ];
453 SIZE_UNITS.iter().any(|u| first_part.ends_with(u))
454 }
455
456 let toks: alloc::vec::Vec<&str> = trimmed.split_whitespace().collect();
457 let mut style: Option<alloc::string::String> = None;
458 let mut weight: Option<alloc::string::String> = None;
459 let mut size_idx = None;
460 for (i, t) in toks.iter().enumerate() {
461 if is_size_token(t) {
462 size_idx = Some(i);
463 break;
464 }
465 match t.to_ascii_lowercase().as_str() {
466 "italic" | "oblique" => style = Some(String::from(*t)),
467 "bold" | "bolder" | "lighter" => weight = Some(String::from(*t)),
468 _ => {
469 if t.parse::<u32>().is_ok() {
470 weight = Some(String::from(*t));
471 }
472 }
474 }
475 }
476 let Some(size_idx) = size_idx else {
477 return;
479 };
480 let size_tok = toks[size_idx];
481 let (size_part, line_height_part) = match size_tok.split_once('/') {
482 Some((s, l)) => (s, Some(l)),
483 None => (size_tok, None),
484 };
485 specified_values
486 .entry(String::from("font-size"))
487 .or_insert_with(|| String::from(size_part));
488 if let Some(lh) = line_height_part {
489 specified_values
490 .entry(String::from("line-height"))
491 .or_insert_with(|| String::from(lh));
492 }
493 if let Some(s) = style {
494 specified_values.entry(String::from("font-style")).or_insert(s);
495 }
496 if let Some(w) = weight {
497 specified_values.entry(String::from("font-weight")).or_insert(w);
498 }
499 let family = toks[size_idx + 1..].join(" ");
500 if !family.is_empty() {
501 specified_values
502 .entry(String::from("font-family"))
503 .or_insert(family);
504 }
505}
506
507pub(crate) fn expand_border_shorthands(specified_values: &mut BTreeMap<String, String>) {
508 if let Some(border) = specified_values.get("border").cloned() {
510 let (width, style, color) = parse_border_shorthand(&border);
511 if let Some(w) = width {
512 specified_values
513 .entry(String::from("border-top-width"))
514 .or_insert(w.clone());
515 specified_values
516 .entry(String::from("border-right-width"))
517 .or_insert(w.clone());
518 specified_values
519 .entry(String::from("border-bottom-width"))
520 .or_insert(w.clone());
521 specified_values
522 .entry(String::from("border-left-width"))
523 .or_insert(w.clone());
524 }
525 if let Some(s) = style {
526 specified_values
527 .entry(String::from("border-top-style"))
528 .or_insert(s.clone());
529 specified_values
530 .entry(String::from("border-right-style"))
531 .or_insert(s.clone());
532 specified_values
533 .entry(String::from("border-bottom-style"))
534 .or_insert(s.clone());
535 specified_values
536 .entry(String::from("border-left-style"))
537 .or_insert(s.clone());
538 }
539 if let Some(c) = color {
540 specified_values
541 .entry(String::from("border-top-color"))
542 .or_insert(c.clone());
543 specified_values
544 .entry(String::from("border-right-color"))
545 .or_insert(c.clone());
546 specified_values
547 .entry(String::from("border-bottom-color"))
548 .or_insert(c.clone());
549 specified_values
550 .entry(String::from("border-left-color"))
551 .or_insert(c.clone());
552 }
553 }
554
555 for side in &["top", "right", "bottom", "left"] {
557 let key = alloc::format!("border-{}", side);
558 if let Some(val) = specified_values.get(&key).cloned() {
559 let (width, style, color) = parse_border_shorthand(&val);
560 if let Some(w) = width {
561 specified_values.insert(alloc::format!("border-{}-width", side), w);
562 }
563 if let Some(s) = style {
564 specified_values.insert(alloc::format!("border-{}-style", side), s);
565 }
566 if let Some(c) = color {
567 specified_values.insert(alloc::format!("border-{}-color", side), c);
568 }
569 }
570 }
571}
572
573pub(crate) fn expand_background_shorthand(specified_values: &mut BTreeMap<String, String>) {
587 let Some(bg) = specified_values.get("background").cloned() else {
588 return;
589 };
590 if let Some((_, after)) = bg.split_once('/') {
596 let toks: Vec<&str> = after
597 .split_whitespace()
598 .take_while(|t| {
599 matches!(*t, "cover" | "contain" | "auto")
600 || t.ends_with('%')
601 || t.ends_with("px")
602 || t.ends_with("em")
603 || t.ends_with("vw")
604 || t.ends_with("vh")
605 })
606 .take(2)
607 .collect();
608 if !toks.is_empty() {
609 specified_values
610 .entry(String::from("background-size"))
611 .or_insert_with(|| toks.join(" "));
612 }
613 }
614 let repeat_toks: Vec<&str> = bg
615 .split_whitespace()
616 .filter(|t| {
617 matches!(
618 *t,
619 "no-repeat" | "repeat" | "repeat-x" | "repeat-y" | "space" | "round"
620 )
621 })
622 .take(2)
623 .collect();
624 if !repeat_toks.is_empty() {
625 specified_values
626 .entry(String::from("background-repeat"))
627 .or_insert_with(|| repeat_toks.join(" "));
628 }
629 let position_part = bg.split_once('/').map(|(before, _)| before).unwrap_or(&bg);
631 let position_toks: Vec<&str> = position_part
632 .split_whitespace()
633 .filter(|t| {
634 matches!(*t, "center" | "top" | "bottom" | "left" | "right")
635 || t.ends_with('%')
636 || t.ends_with("px")
637 })
638 .collect();
639 if !position_toks.is_empty() {
640 specified_values
641 .entry(String::from("background-position"))
642 .or_insert_with(|| position_toks.join(" "));
643 }
644}
645
646pub fn apply_text_transform(text: &str, mode: &str) -> String {
648 match mode.trim() {
649 "uppercase" => text.to_uppercase(),
650 "lowercase" => text.to_lowercase(),
651 "capitalize" => {
652 let mut out = String::with_capacity(text.len());
654 let mut at_word_start = true;
655 for c in text.chars() {
656 if c.is_whitespace() {
657 at_word_start = true;
658 out.push(c);
659 } else if at_word_start {
660 out.extend(c.to_uppercase());
661 at_word_start = false;
662 } else {
663 out.push(c);
664 }
665 }
666 out
667 }
668 _ => text.to_string(),
669 }
670}
671
672