1use super::*;
4
5fn resolve_or_add_layer(
6 raw_name: &str,
7 layer_names: &mut Vec<String>,
8 parent_prefix: Option<&str>,
9) -> usize {
10 let raw_name = raw_name.trim();
11 let full_name = if raw_name.is_empty() {
12 let anon_id = layer_names.len();
13 match parent_prefix {
14 Some(p) => alloc::format!("{}.<anon-{}>", p, anon_id),
15 None => alloc::format!("<anon-{}>", anon_id),
16 }
17 } else {
18 match parent_prefix {
19 Some(p) => alloc::format!("{}.{}", p, raw_name),
20 None => raw_name.to_string(),
21 }
22 };
23
24 if let Some(pos) = layer_names.iter().position(|n| *n == full_name) {
25 pos
26 } else {
27 layer_names.push(full_name);
28 layer_names.len() - 1
29 }
30}
31
32pub fn parse_css(css: &str) -> StyleSheet {
33 let mut layer_names = Vec::new();
34 parse_css_with_layers(css, &mut layer_names)
35}
36
37pub fn parse_css_with_layers(css: &str, layer_names: &mut Vec<String>) -> StyleSheet {
38 let css_clean = strip_css_comments(css);
40 parse_css_internal(&css_clean, layer_names, None)
41}
42
43fn parse_css_internal(
44 css_clean: &str,
45 layer_names: &mut Vec<String>,
46 parent_prefix: Option<&str>,
47) -> StyleSheet {
48 let mut rules = Vec::new();
49 let mut keyframes: Vec<Keyframes> = Vec::new();
50 let mut counter_styles: Vec<CounterStyleDef> = Vec::new();
51 let mut font_faces: Vec<FontFaceDef> = Vec::new();
52
53 let items = split_css_items(css_clean);
54
55 for item in items {
56 match item {
57 CssItem::Statement(stmt) => {
58 let lower = stmt.to_lowercase();
59 if lower.starts_with("@layer") {
60 let rest = stmt.trim_start_matches("@layer").trim();
61 for name in rest.split(',') {
62 let name = name.trim();
63 if !name.is_empty() {
64 resolve_or_add_layer(name, layer_names, parent_prefix);
65 }
66 }
67 }
68 }
69 CssItem::Block(selector_part, decl_part) => {
70 let selector_part = selector_part.trim();
71 let decl_part = decl_part.trim();
72 if selector_part.is_empty() || decl_part.is_empty() {
73 continue;
74 }
75
76 if selector_part.starts_with('@') {
77 let lower = selector_part.to_lowercase();
80 if lower.starts_with("@font-face") {
81 if let Some(def) = parse_font_face_def(decl_part) {
82 font_faces.push(def);
83 }
84 } else if lower.starts_with("@keyframes") || lower.starts_with("@-webkit-keyframes") {
85 let name = selector_part
86 .trim_start_matches("@-webkit-keyframes")
87 .trim_start_matches("@keyframes")
88 .trim()
89 .to_string();
90 if !name.is_empty() {
91 if let Some(kf) = parse_keyframes(&name, decl_part) {
92 keyframes.push(kf);
93 }
94 }
95 } else if lower.starts_with("@media") {
96 let query = selector_part
97 .trim_start_matches("@media")
98 .trim()
99 .to_lowercase();
100 let inner_rules = parse_css_declarations(decl_part);
101 for mut r in inner_rules {
102 r.media_query = Some(match r.media_query {
103 Some(existing) => alloc::format!("{} and {}", existing, query),
104 None => query.clone(),
105 });
106 rules.push(r);
107 }
108 } else if lower.starts_with("@container") {
109 let prelude = selector_part.trim_start_matches("@container").trim();
110 let (name, cond) =
111 super::container_query::split_name_and_condition(prelude);
112 let tag = alloc::format!("{}|{}", name.unwrap_or_default(), cond);
113 let inner_rules = parse_css_declarations(decl_part);
114 for mut r in inner_rules {
115 r.container_query = Some(match r.container_query {
116 Some(existing) => alloc::format!("{}&&{}", existing, tag),
117 None => tag.clone(),
118 });
119 rules.push(r);
120 }
121 } else if lower.starts_with("@supports") {
122 let cond = selector_part.trim_start_matches("@supports").trim().to_lowercase();
123 if supports_condition_matches(&cond) {
124 let inner_rules = parse_css_declarations(decl_part);
125 rules.extend(inner_rules);
126 }
127 } else if lower.starts_with("@counter-style") {
128 let name = selector_part
129 .trim_start_matches("@counter-style")
130 .trim()
131 .to_string();
132 if !name.is_empty() {
133 if let Some(def) = parse_counter_style_def(&name, decl_part) {
134 counter_styles.push(def);
135 }
136 }
137 } else if lower.starts_with("@layer") {
138 let raw_name = selector_part.trim_start_matches("@layer").trim();
140 let layer_idx = resolve_or_add_layer(raw_name, layer_names, parent_prefix);
141 let child_prefix = layer_names[layer_idx].clone();
142 let inner_sheet = parse_css_internal(decl_part, layer_names, Some(&child_prefix));
143 for mut r in inner_sheet.rules {
144 if r.layer.is_none() {
145 r.layer = Some(layer_idx);
146 }
147 rules.push(r);
148 }
149 keyframes.extend(inner_sheet.keyframes);
150 counter_styles.extend(inner_sheet.counter_styles);
151 font_faces.extend(inner_sheet.font_faces);
152 }
153 continue;
154 }
155
156 let selectors: Vec<Selector> = split_top_level_commas(selector_part)
159 .into_iter()
160 .filter_map(|s| parse_selector(s.trim()))
161 .collect();
162 if selectors.is_empty() {
163 continue;
164 }
165
166 let parent_selector_texts: Vec<String> = split_top_level_commas(selector_part)
168 .into_iter()
169 .map(|s| s.trim().to_string())
170 .collect();
171 let declarations = parse_nested_body(&parent_selector_texts, decl_part, &mut rules, None);
172 if declarations.is_empty() {
173 continue;
174 }
175
176 rules.push(Rule {
177 selectors,
178 declarations,
179 layer: None,
180 media_query: None,
181 container_query: None,
182 });
183 }
184 }
185 }
186
187 StyleSheet {
188 rules,
189 keyframes,
190 counter_styles,
191 font_faces,
192 }
193}
194
195fn parse_font_face_def(decl_part: &str) -> Option<FontFaceDef> {
198 let mut family = None;
199 let mut best_url: Option<String> = None;
200 let mut best_is_preferred_format = false;
201 let mut weight: u16 = 400;
203 for decl in decl_part.split(';') {
204 let decl = decl.trim();
205 let Some((prop, val)) = decl.split_once(':') else {
206 continue;
207 };
208 let prop = prop.trim().to_lowercase();
209 let val = val.trim();
210 if prop == "font-family" {
211 let name = val.trim_matches(|c| c == '\'' || c == '"').trim();
212 if !name.is_empty() {
213 family = Some(name.to_string());
214 }
215 } else if prop == "font-weight" {
216 let first = val.split_whitespace().next().unwrap_or("");
219 weight = match first.to_lowercase().as_str() {
220 "bold" => 700,
221 "normal" => 400,
222 other => other.parse::<u16>().unwrap_or(400),
223 };
224 } else if prop == "src" {
225 for part in val.split(',') {
228 let part = part.trim();
229 let Some(after_url) = part.strip_prefix("url(") else {
230 continue;
231 };
232 let Some((url_raw, rest)) = after_url.split_once(')') else {
233 continue;
234 };
235 let url = url_raw.trim_matches(|c| c == '\'' || c == '"').trim();
236 if url.is_empty() {
237 continue;
238 }
239 let rest_lower = rest.to_lowercase();
240 let is_preferred = rest_lower.contains("truetype")
241 || rest_lower.contains("opentype")
242 || rest_lower.contains("woff2");
243 if best_url.is_none() || (is_preferred && !best_is_preferred_format) {
244 best_url = Some(url.to_string());
245 best_is_preferred_format = is_preferred;
246 }
247 }
248 }
249 }
250 let family = family?;
251 Some(FontFaceDef {
252 family,
253 src_url: best_url,
254 weight,
255 })
256}
257
258pub(crate) fn parse_counter_style_def(name: &str, decl_part: &str) -> Option<CounterStyleDef> {
262 let mut system = String::from("cyclic");
263 let mut symbols: Vec<String> = Vec::new();
264 let mut suffix = String::from(". ");
265 for decl in decl_part.split(';') {
266 let mut parts = decl.splitn(2, ':');
267 let key = parts.next()?.trim().to_lowercase();
268 let Some(val) = parts.next() else { continue };
269 let val = val.trim();
270 match key.as_str() {
271 "system" => system = val.to_lowercase(),
272 "symbols" => {
273 symbols = split_top_level_commas(val)
274 .into_iter()
275 .flat_map(|seg| seg.split_whitespace())
276 .map(|s| String::from(s.trim_matches(|c| c == '"' || c == '\'')))
277 .filter(|s| !s.is_empty())
278 .collect();
279 }
280 "suffix" => {
281 suffix = String::from(val.trim_matches(|c| c == '"' || c == '\''));
282 }
283 _ => {}
284 }
285 }
286 if symbols.is_empty() {
287 return None;
288 }
289 Some(CounterStyleDef {
290 name: String::from(name),
291 system,
292 symbols,
293 suffix,
294 })
295}
296
297pub(crate) fn parse_keyframes(name: &str, body: &str) -> Option<Keyframes> {
299 let blocks = split_css_blocks(body);
300 let mut frames: Vec<(f32, Vec<Declaration>)> = Vec::new();
301 for (sel, decl_part) in blocks {
302 let decls = parse_declaration_list(&decl_part);
304 if decls.is_empty() {
305 continue;
306 }
307 for off_tok in sel.split(',') {
308 let tok = off_tok.trim().to_lowercase();
309 let offset = match tok.as_str() {
310 "from" => 0.0,
311 "to" => 1.0,
312 _ => match tok.trim_end_matches('%').trim().parse::<f32>() {
313 Ok(p) => p / 100.0,
314 Err(_) => continue,
315 },
316 };
317 if (0.0..=1.0).contains(&offset) {
318 frames.push((offset, decls.clone()));
319 }
320 }
321 }
322 if frames.is_empty() {
323 return None;
324 }
325 frames.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
326 Some(Keyframes {
327 name: name.to_string(),
328 frames,
329 })
330}
331
332pub(crate) fn parse_declaration_list(decl_part: &str) -> Vec<Declaration> {
334 decl_part
335 .split(';')
336 .filter_map(|decl| {
337 let mut kv = decl.splitn(2, ':');
338 let name = kv.next()?.trim();
339 let raw_value = kv.next()?.trim();
340 let (value, important) = match raw_value.rfind('!') {
341 Some(pos)
342 if raw_value
343 .get(pos + 1..)
344 .unwrap_or("")
345 .trim()
346 .eq_ignore_ascii_case("important") =>
347 {
348 (raw_value.get(..pos).unwrap_or("").trim(), true)
349 }
350 _ => (raw_value, false),
351 };
352 if name.is_empty() || value.is_empty() {
353 return None;
354 }
355 Some(Declaration {
356 name: String::from(name),
357 value: String::from(value),
358 important,
359 })
360 })
361 .collect()
362}
363
364pub(crate) fn strip_css_comments(css: &str) -> String {
371 if !css.contains("/*") {
372 return css.to_string();
373 }
374 let bytes = css.as_bytes();
375 let mut out = Vec::with_capacity(css.len());
376 let mut i = 0;
377 while i < bytes.len() {
378 if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' {
379 i += 2;
381 while i + 1 < bytes.len() {
382 if bytes[i] == b'*' && bytes[i + 1] == b'/' {
383 i += 2;
384 break;
385 }
386 i += 1;
387 }
388 } else {
389 out.push(bytes[i]);
390 i += 1;
391 }
392 }
393 String::from_utf8(out).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
394}
395
396pub fn supports_condition_matches(cond: &str) -> bool {
401 let cond = cond.trim();
402 if cond.is_empty() {
403 return true;
404 }
405 if cond.contains(" and ") && !cond.contains(" or ") {
406 return cond.split(" and ").all(|p| supports_condition_matches(p.trim()));
407 }
408 if cond.contains(" or ") && !cond.contains(" and ") {
409 return cond.split(" or ").any(|p| supports_condition_matches(p.trim()));
410 }
411 if let Some(rest) = cond.strip_prefix("not ") {
412 return !supports_condition_matches(rest.trim());
413 }
414 if let Some(stripped) = cond.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
415 return supports_condition_matches(stripped.trim());
416 }
417 true
419}
420
421pub fn media_query_matches(query: &str) -> bool {
424 let vw = VIEWPORT_HINT_W.load(AtomicOrd::Relaxed);
425 let vh = VIEWPORT_HINT_H.load(AtomicOrd::Relaxed);
426 if query == "screen" || query == "all" || query.is_empty() {
428 return true;
429 }
430 if query == "print" {
432 return false;
433 }
434 if query == "not print" {
436 return true;
437 }
438 if query.contains(',') {
440 return query.split(',').any(|q| media_query_matches(q.trim()));
441 }
442 if query.contains(" and ") {
448 return query.split(" and ").all(|part| media_query_matches(part.trim()));
449 }
450 let inner = query.trim_start_matches("screen").trim();
453 let cond = inner.trim_matches(|c| c == '(' || c == ')');
455 if let Some(rest) = cond
457 .strip_prefix("max-width:")
458 .or_else(|| cond.strip_prefix("max-width :"))
459 {
460 let px: i32 = rest
461 .trim()
462 .trim_end_matches("px")
463 .trim()
464 .parse()
465 .unwrap_or(i32::MAX);
466 return vw <= px;
467 }
468 if let Some(rest) = cond
470 .strip_prefix("min-width:")
471 .or_else(|| cond.strip_prefix("min-width :"))
472 {
473 let px: i32 = rest
474 .trim()
475 .trim_end_matches("px")
476 .trim()
477 .parse()
478 .unwrap_or(0);
479 return vw >= px;
480 }
481 if let Some(rest) = cond
483 .strip_prefix("max-height:")
484 .or_else(|| cond.strip_prefix("max-height :"))
485 {
486 let px: i32 = rest
487 .trim()
488 .trim_end_matches("px")
489 .trim()
490 .parse()
491 .unwrap_or(i32::MAX);
492 return vh <= px;
493 }
494 if let Some(rest) = cond
496 .strip_prefix("min-height:")
497 .or_else(|| cond.strip_prefix("min-height :"))
498 {
499 let px: i32 = rest
500 .trim()
501 .trim_end_matches("px")
502 .trim()
503 .parse()
504 .unwrap_or(0);
505 return vh >= px;
506 }
507 if let Some(rest) = cond.strip_prefix("orientation:") {
509 let o = rest.trim();
510 if o == "landscape" {
511 return vw >= vh;
512 }
513 if o == "portrait" {
514 return vh > vw;
515 }
516 }
517 let parse_dppx = |s: &str| -> Option<f32> {
523 let s = s.trim();
524 if let Some(n) = s.strip_suffix("dppx") {
525 n.trim().parse::<f32>().ok()
526 } else if let Some(n) = s.strip_suffix("dpi") {
527 n.trim().parse::<f32>().ok().map(|v| v / 96.0)
528 } else if let Some(n) = s.strip_suffix("dpcm") {
529 n.trim().parse::<f32>().ok().map(|v| v / 37.795_28)
530 } else if let Some(n) = s.strip_suffix('x') {
531 n.trim().parse::<f32>().ok()
532 } else {
533 None
534 }
535 };
536 const DEVICE_DPPX: f32 = 1.0;
537 if let Some(rest) = cond.strip_prefix("min-resolution:") {
538 return parse_dppx(rest).is_some_and(|r| DEVICE_DPPX >= r);
539 }
540 if let Some(rest) = cond.strip_prefix("max-resolution:") {
541 return parse_dppx(rest).is_some_and(|r| DEVICE_DPPX <= r);
542 }
543 if let Some(rest) = cond.strip_prefix("resolution:") {
544 return parse_dppx(rest).is_some_and(|r| (DEVICE_DPPX - r).abs() < 0.001);
545 }
546 if let Some(rest) = cond.strip_prefix("min-monochrome:") {
549 return rest.trim().parse::<i32>().is_ok_and(|n| n <= 0);
550 }
551 if let Some(rest) = cond.strip_prefix("max-monochrome:") {
552 return rest.trim().parse::<i32>().is_ok_and(|n| n >= 0);
553 }
554 if let Some(rest) = cond.strip_prefix("monochrome:") {
555 return rest.trim().parse::<i32>() == Ok(0);
556 }
557 if cond.trim() == "monochrome" {
558 return false;
559 }
560 if let Some(rest) = cond.strip_prefix("prefers-color-scheme:") {
564 let scheme = rest.trim();
565 let is_dark = PREFERS_DARK.load(AtomicOrd::Relaxed);
566 if scheme == "dark" {
567 return is_dark;
568 }
569 if scheme == "light" {
570 return !is_dark;
571 }
572 }
573 if let Some(rest) = cond.strip_prefix("prefers-reduced-motion:") {
576 let pref = rest.trim();
577 return pref == "no-preference";
578 }
579 if let Some(rest) = cond.strip_prefix("prefers-contrast:") {
585 let pref = rest.trim();
586 return pref == "no-preference";
587 }
588 if let Some(rest) = cond.strip_prefix("any-pointer:") {
595 return rest.trim() == "fine";
596 }
597 if let Some(rest) = cond.strip_prefix("pointer:") {
598 return rest.trim() == "fine";
599 }
600 if let Some(rest) = cond.strip_prefix("any-hover:") {
601 return rest.trim() == "hover";
602 }
603 if let Some(rest) = cond.strip_prefix("hover:") {
604 return rest.trim() == "hover";
605 }
606 if let Some(rest) = cond.strip_prefix("scripting:") {
613 return rest.trim() == "enabled";
614 }
615 if let Some(rest) = cond.strip_prefix("forced-colors:") {
616 return rest.trim() == "none";
617 }
618 if let Some(rest) = cond.strip_prefix("inverted-colors:") {
619 return rest.trim() == "none";
620 }
621 if let Some(rest) = cond.strip_prefix("color-gamut:") {
622 return rest.trim() == "srgb";
623 }
624 if let Some(rest) = cond.strip_prefix("update:") {
632 return rest.trim() == "fast";
633 }
634 if let Some(rest) = cond.strip_prefix("grid:") {
635 return rest.trim() == "0";
636 }
637 if let Some(rest) = cond.strip_prefix("prefers-reduced-transparency:") {
638 return rest.trim() == "no-preference";
639 }
640 if let Some(rest) = cond.strip_prefix("prefers-reduced-data:") {
641 return rest.trim() == "no-preference";
642 }
643 true }
645
646pub(crate) fn parse_css_declarations(css: &str) -> Vec<Rule> {
648 let mut rules = Vec::new();
649 let blocks = split_css_blocks(css);
650 for (selector_part, decl_part) in blocks {
651 let selector_part = selector_part.trim();
652 let decl_part = decl_part.trim();
653 if selector_part.is_empty() || decl_part.is_empty() {
654 continue;
655 }
656 if selector_part.starts_with('@') {
657 continue;
658 }
659 let selectors: Vec<Selector> = split_top_level_commas(selector_part)
662 .into_iter()
663 .filter_map(|s| parse_selector(s.trim()))
664 .collect();
665 if selectors.is_empty() {
666 continue;
667 }
668 let decl_part = match decl_part.find('{') {
672 Some(pos) => decl_part.get(..pos).unwrap_or(decl_part).trim(),
673 None => decl_part,
674 };
675 let declarations: Vec<Declaration> = decl_part
676 .split(';')
677 .filter_map(|decl| {
678 let mut kv = decl.splitn(2, ':');
679 let name = kv.next()?.trim();
680 let raw_value = kv.next()?.trim();
681 let (value, important) = match raw_value.rfind('!') {
682 Some(pos)
683 if raw_value
684 .get(pos + 1..)
685 .unwrap_or("")
686 .trim()
687 .eq_ignore_ascii_case("important") =>
688 {
689 (raw_value.get(..pos).unwrap_or("").trim(), true)
690 }
691 _ => (raw_value, false),
692 };
693 if name.is_empty() || value.is_empty() {
694 return None;
695 }
696 Some(Declaration {
697 name: String::from(name),
698 value: String::from(value),
699 important,
700 })
701 })
702 .collect();
703 if !declarations.is_empty() {
704 rules.push(Rule {
705 selectors,
706 declarations,
707 layer: None,
708 media_query: None,
709 container_query: None,
710 });
711 }
712 }
713 rules
714}
715
716#[derive(Debug, Clone, PartialEq, Eq)]
717pub(crate) enum CssItem {
718 Statement(String),
719 Block(String, String),
720}
721
722pub(crate) fn split_css_items(css: &str) -> Vec<CssItem> {
723 let mut items = Vec::new();
724 let mut depth = 0;
725 let mut item_start = 0;
726 let mut block_start = 0;
727 let bytes = css.as_bytes();
728 let mut in_quote: Option<u8> = None;
729 let mut escaped = false;
730
731 for i in 0..bytes.len() {
732 let b = bytes[i];
733 if escaped {
734 escaped = false;
735 continue;
736 }
737 if b == b'\\' {
738 escaped = true;
739 continue;
740 }
741 if let Some(q) = in_quote {
742 if b == q {
743 in_quote = None;
744 }
745 continue;
746 }
747 if b == b'"' || b == b'\'' {
748 in_quote = Some(b);
749 continue;
750 }
751
752 match b {
753 b';' if depth == 0 => {
754 if let Some(stmt) = css.get(item_start..i) {
755 let trimmed = stmt.trim();
756 if !trimmed.is_empty() {
757 items.push(CssItem::Statement(trimmed.to_string()));
758 }
759 }
760 item_start = i + 1;
761 }
762 b'{' => {
763 if depth == 0 {
764 block_start = i + 1;
765 }
766 depth += 1;
767 }
768 b'}' => {
769 if depth > 0 {
770 depth -= 1;
771 if depth == 0 {
772 if let (Some(selector), Some(body)) = (
773 css.get(item_start..block_start.saturating_sub(1)),
774 css.get(block_start..i),
775 ) {
776 items.push(CssItem::Block(selector.trim().to_string(), body.to_string()));
777 }
778 item_start = i + 1;
779 }
780 }
781 }
782 _ => {}
783 }
784 }
785
786 items
787}
788
789pub(crate) fn split_css_blocks(css: &str) -> Vec<(String, String)> {
790 split_css_items(css)
791 .into_iter()
792 .filter_map(|item| match item {
793 CssItem::Block(sel, body) => Some((sel, body)),
794 CssItem::Statement(_) => None,
795 })
796 .collect()
797}
798
799