1use super::*;
4
5pub fn parse_css(css: &str) -> StyleSheet {
6 let css_clean = strip_css_comments(css);
8
9 let mut rules = Vec::new();
11 let mut keyframes: Vec<Keyframes> = Vec::new();
12 let mut counter_styles: Vec<CounterStyleDef> = Vec::new();
13 let mut font_faces: Vec<FontFaceDef> = Vec::new();
14 let mut layer_names: Vec<String> = Vec::new();
18 let blocks = split_css_blocks(&css_clean);
19
20 for (selector_part, decl_part) in blocks {
21 let selector_part = selector_part.trim();
22 let decl_part = decl_part.trim();
23 if selector_part.is_empty() || decl_part.is_empty() {
24 continue;
25 }
26
27 if selector_part.starts_with('@') {
28 let lower = selector_part.to_lowercase();
31 if lower.starts_with("@font-face") {
32 if let Some(def) = parse_font_face_def(decl_part) {
33 font_faces.push(def);
34 }
35 } else if lower.starts_with("@keyframes") || lower.starts_with("@-webkit-keyframes") {
36 let name = selector_part
37 .trim_start_matches("@-webkit-keyframes")
38 .trim_start_matches("@keyframes")
39 .trim()
40 .to_string();
41 if !name.is_empty() {
42 if let Some(kf) = parse_keyframes(&name, decl_part) {
43 keyframes.push(kf);
44 }
45 }
46 } else if lower.starts_with("@media") {
47 let query = selector_part
55 .trim_start_matches("@media")
56 .trim()
57 .to_lowercase();
58 let inner_rules = parse_css_declarations(decl_part);
59 for mut r in inner_rules {
60 r.media_query = Some(match r.media_query {
61 Some(existing) => alloc::format!("{} and {}", existing, query),
62 None => query.clone(),
63 });
64 rules.push(r);
65 }
66 } else if lower.starts_with("@container") {
67 let prelude = selector_part.trim_start_matches("@container").trim();
78 let (name, cond) =
79 super::container_query::split_name_and_condition(prelude);
80 let tag = alloc::format!("{}|{}", name.unwrap_or_default(), cond);
81 let inner_rules = parse_css_declarations(decl_part);
82 for mut r in inner_rules {
83 r.container_query = Some(match r.container_query {
85 Some(existing) => alloc::format!("{}&&{}", existing, tag),
86 None => tag.clone(),
87 });
88 rules.push(r);
89 }
90 } else if lower.starts_with("@supports") {
91 let cond = selector_part.trim_start_matches("@supports").trim().to_lowercase();
100 if supports_condition_matches(&cond) {
101 let inner_rules = parse_css_declarations(decl_part);
102 rules.extend(inner_rules);
103 }
104 } else if lower.starts_with("@counter-style") {
105 let name = selector_part
106 .trim_start_matches("@counter-style")
107 .trim()
108 .to_string();
109 if !name.is_empty() {
110 if let Some(def) = parse_counter_style_def(&name, decl_part) {
111 counter_styles.push(def);
112 }
113 }
114 } else if lower.starts_with("@layer") {
115 let name = selector_part.trim_start_matches("@layer").trim().to_string();
119 let layer_idx = if name.is_empty() {
120 layer_names.push(alloc::format!("<anon-{}>", layer_names.len()));
121 layer_names.len() - 1
122 } else if let Some(pos) = layer_names.iter().position(|n| *n == name) {
123 pos
124 } else {
125 layer_names.push(name);
126 layer_names.len() - 1
127 };
128 let inner_sheet = parse_css(decl_part);
131 for mut r in inner_sheet.rules {
132 r.layer = Some(layer_idx);
133 rules.push(r);
134 }
135 keyframes.extend(inner_sheet.keyframes);
136 counter_styles.extend(inner_sheet.counter_styles);
137 font_faces.extend(inner_sheet.font_faces);
138 }
139 continue;
140 }
141
142 let selectors: Vec<Selector> = split_top_level_commas(selector_part)
145 .into_iter()
146 .filter_map(|s| parse_selector(s.trim()))
147 .collect();
148 if selectors.is_empty() {
149 continue;
150 }
151
152 let parent_selector_texts: Vec<String> = split_top_level_commas(selector_part)
158 .into_iter()
159 .map(|s| s.trim().to_string())
160 .collect();
161 let declarations = parse_nested_body(&parent_selector_texts, decl_part, &mut rules, None);
162 if declarations.is_empty() {
163 continue;
164 }
165
166 rules.push(Rule {
167 selectors,
168 declarations,
169 layer: None,
170 media_query: None,
171 container_query: None,
172 });
173 }
174
175 StyleSheet {
176 rules,
177 keyframes,
178 counter_styles,
179 font_faces,
180 }
181}
182
183fn parse_font_face_def(decl_part: &str) -> Option<FontFaceDef> {
186 let mut family = None;
187 let mut best_url: Option<String> = None;
188 let mut best_is_preferred_format = false;
189 let mut weight: u16 = 400;
191 for decl in decl_part.split(';') {
192 let decl = decl.trim();
193 let Some((prop, val)) = decl.split_once(':') else {
194 continue;
195 };
196 let prop = prop.trim().to_lowercase();
197 let val = val.trim();
198 if prop == "font-family" {
199 let name = val.trim_matches(|c| c == '\'' || c == '"').trim();
200 if !name.is_empty() {
201 family = Some(name.to_string());
202 }
203 } else if prop == "font-weight" {
204 let first = val.split_whitespace().next().unwrap_or("");
207 weight = match first.to_lowercase().as_str() {
208 "bold" => 700,
209 "normal" => 400,
210 other => other.parse::<u16>().unwrap_or(400),
211 };
212 } else if prop == "src" {
213 for part in val.split(',') {
216 let part = part.trim();
217 let Some(after_url) = part.strip_prefix("url(") else {
218 continue;
219 };
220 let Some((url_raw, rest)) = after_url.split_once(')') else {
221 continue;
222 };
223 let url = url_raw.trim_matches(|c| c == '\'' || c == '"').trim();
224 if url.is_empty() {
225 continue;
226 }
227 let rest_lower = rest.to_lowercase();
228 let is_preferred = rest_lower.contains("truetype")
229 || rest_lower.contains("opentype")
230 || rest_lower.contains("woff2");
231 if best_url.is_none() || (is_preferred && !best_is_preferred_format) {
232 best_url = Some(url.to_string());
233 best_is_preferred_format = is_preferred;
234 }
235 }
236 }
237 }
238 let family = family?;
239 Some(FontFaceDef {
240 family,
241 src_url: best_url,
242 weight,
243 })
244}
245
246pub(crate) fn parse_counter_style_def(name: &str, decl_part: &str) -> Option<CounterStyleDef> {
250 let mut system = String::from("cyclic");
251 let mut symbols: Vec<String> = Vec::new();
252 let mut suffix = String::from(". ");
253 for decl in decl_part.split(';') {
254 let mut parts = decl.splitn(2, ':');
255 let key = parts.next()?.trim().to_lowercase();
256 let Some(val) = parts.next() else { continue };
257 let val = val.trim();
258 match key.as_str() {
259 "system" => system = val.to_lowercase(),
260 "symbols" => {
261 symbols = split_top_level_commas(val)
262 .into_iter()
263 .flat_map(|seg| seg.split_whitespace())
264 .map(|s| String::from(s.trim_matches(|c| c == '"' || c == '\'')))
265 .filter(|s| !s.is_empty())
266 .collect();
267 }
268 "suffix" => {
269 suffix = String::from(val.trim_matches(|c| c == '"' || c == '\''));
270 }
271 _ => {}
272 }
273 }
274 if symbols.is_empty() {
275 return None;
276 }
277 Some(CounterStyleDef {
278 name: String::from(name),
279 system,
280 symbols,
281 suffix,
282 })
283}
284
285pub(crate) fn parse_keyframes(name: &str, body: &str) -> Option<Keyframes> {
287 let blocks = split_css_blocks(body);
288 let mut frames: Vec<(f32, Vec<Declaration>)> = Vec::new();
289 for (sel, decl_part) in blocks {
290 let decls = parse_declaration_list(&decl_part);
292 if decls.is_empty() {
293 continue;
294 }
295 for off_tok in sel.split(',') {
296 let tok = off_tok.trim().to_lowercase();
297 let offset = match tok.as_str() {
298 "from" => 0.0,
299 "to" => 1.0,
300 _ => match tok.trim_end_matches('%').trim().parse::<f32>() {
301 Ok(p) => p / 100.0,
302 Err(_) => continue,
303 },
304 };
305 if (0.0..=1.0).contains(&offset) {
306 frames.push((offset, decls.clone()));
307 }
308 }
309 }
310 if frames.is_empty() {
311 return None;
312 }
313 frames.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
314 Some(Keyframes {
315 name: name.to_string(),
316 frames,
317 })
318}
319
320pub(crate) fn parse_declaration_list(decl_part: &str) -> Vec<Declaration> {
322 decl_part
323 .split(';')
324 .filter_map(|decl| {
325 let mut kv = decl.splitn(2, ':');
326 let name = kv.next()?.trim();
327 let raw_value = kv.next()?.trim();
328 let (value, important) = match raw_value.rfind('!') {
329 Some(pos)
330 if raw_value
331 .get(pos + 1..)
332 .unwrap_or("")
333 .trim()
334 .eq_ignore_ascii_case("important") =>
335 {
336 (raw_value.get(..pos).unwrap_or("").trim(), true)
337 }
338 _ => (raw_value, false),
339 };
340 if name.is_empty() || value.is_empty() {
341 return None;
342 }
343 Some(Declaration {
344 name: String::from(name),
345 value: String::from(value),
346 important,
347 })
348 })
349 .collect()
350}
351
352pub(crate) fn strip_css_comments(css: &str) -> String {
359 if !css.contains("/*") {
360 return css.to_string();
361 }
362 let bytes = css.as_bytes();
363 let mut out = Vec::with_capacity(css.len());
364 let mut i = 0;
365 while i < bytes.len() {
366 if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' {
367 i += 2;
369 while i + 1 < bytes.len() {
370 if bytes[i] == b'*' && bytes[i + 1] == b'/' {
371 i += 2;
372 break;
373 }
374 i += 1;
375 }
376 } else {
377 out.push(bytes[i]);
378 i += 1;
379 }
380 }
381 String::from_utf8(out).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
382}
383
384pub fn supports_condition_matches(cond: &str) -> bool {
389 let cond = cond.trim();
390 if cond.is_empty() {
391 return true;
392 }
393 if cond.contains(" and ") && !cond.contains(" or ") {
394 return cond.split(" and ").all(|p| supports_condition_matches(p.trim()));
395 }
396 if cond.contains(" or ") && !cond.contains(" and ") {
397 return cond.split(" or ").any(|p| supports_condition_matches(p.trim()));
398 }
399 if let Some(rest) = cond.strip_prefix("not ") {
400 return !supports_condition_matches(rest.trim());
401 }
402 if let Some(stripped) = cond.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
403 return supports_condition_matches(stripped.trim());
404 }
405 true
407}
408
409pub fn media_query_matches(query: &str) -> bool {
412 let vw = VIEWPORT_HINT_W.load(AtomicOrd::Relaxed);
413 let vh = VIEWPORT_HINT_H.load(AtomicOrd::Relaxed);
414 if query == "screen" || query == "all" || query.is_empty() {
416 return true;
417 }
418 if query == "print" {
420 return false;
421 }
422 if query == "not print" {
424 return true;
425 }
426 if query.contains(',') {
428 return query.split(',').any(|q| media_query_matches(q.trim()));
429 }
430 if query.contains(" and ") {
436 return query.split(" and ").all(|part| media_query_matches(part.trim()));
437 }
438 let inner = query.trim_start_matches("screen").trim();
441 let cond = inner.trim_matches(|c| c == '(' || c == ')');
443 if let Some(rest) = cond
445 .strip_prefix("max-width:")
446 .or_else(|| cond.strip_prefix("max-width :"))
447 {
448 let px: i32 = rest
449 .trim()
450 .trim_end_matches("px")
451 .trim()
452 .parse()
453 .unwrap_or(i32::MAX);
454 return vw <= px;
455 }
456 if let Some(rest) = cond
458 .strip_prefix("min-width:")
459 .or_else(|| cond.strip_prefix("min-width :"))
460 {
461 let px: i32 = rest
462 .trim()
463 .trim_end_matches("px")
464 .trim()
465 .parse()
466 .unwrap_or(0);
467 return vw >= px;
468 }
469 if let Some(rest) = cond
471 .strip_prefix("max-height:")
472 .or_else(|| cond.strip_prefix("max-height :"))
473 {
474 let px: i32 = rest
475 .trim()
476 .trim_end_matches("px")
477 .trim()
478 .parse()
479 .unwrap_or(i32::MAX);
480 return vh <= px;
481 }
482 if let Some(rest) = cond
484 .strip_prefix("min-height:")
485 .or_else(|| cond.strip_prefix("min-height :"))
486 {
487 let px: i32 = rest
488 .trim()
489 .trim_end_matches("px")
490 .trim()
491 .parse()
492 .unwrap_or(0);
493 return vh >= px;
494 }
495 if let Some(rest) = cond.strip_prefix("orientation:") {
497 let o = rest.trim();
498 if o == "landscape" {
499 return vw >= vh;
500 }
501 if o == "portrait" {
502 return vh > vw;
503 }
504 }
505 let parse_dppx = |s: &str| -> Option<f32> {
511 let s = s.trim();
512 if let Some(n) = s.strip_suffix("dppx") {
513 n.trim().parse::<f32>().ok()
514 } else if let Some(n) = s.strip_suffix("dpi") {
515 n.trim().parse::<f32>().ok().map(|v| v / 96.0)
516 } else if let Some(n) = s.strip_suffix("dpcm") {
517 n.trim().parse::<f32>().ok().map(|v| v / 37.795_28)
518 } else if let Some(n) = s.strip_suffix('x') {
519 n.trim().parse::<f32>().ok()
520 } else {
521 None
522 }
523 };
524 const DEVICE_DPPX: f32 = 1.0;
525 if let Some(rest) = cond.strip_prefix("min-resolution:") {
526 return parse_dppx(rest).is_some_and(|r| DEVICE_DPPX >= r);
527 }
528 if let Some(rest) = cond.strip_prefix("max-resolution:") {
529 return parse_dppx(rest).is_some_and(|r| DEVICE_DPPX <= r);
530 }
531 if let Some(rest) = cond.strip_prefix("resolution:") {
532 return parse_dppx(rest).is_some_and(|r| (DEVICE_DPPX - r).abs() < 0.001);
533 }
534 if let Some(rest) = cond.strip_prefix("min-monochrome:") {
537 return rest.trim().parse::<i32>().is_ok_and(|n| n <= 0);
538 }
539 if let Some(rest) = cond.strip_prefix("max-monochrome:") {
540 return rest.trim().parse::<i32>().is_ok_and(|n| n >= 0);
541 }
542 if let Some(rest) = cond.strip_prefix("monochrome:") {
543 return rest.trim().parse::<i32>() == Ok(0);
544 }
545 if cond.trim() == "monochrome" {
546 return false;
547 }
548 if let Some(rest) = cond.strip_prefix("prefers-color-scheme:") {
552 let scheme = rest.trim();
553 let is_dark = PREFERS_DARK.load(AtomicOrd::Relaxed);
554 if scheme == "dark" {
555 return is_dark;
556 }
557 if scheme == "light" {
558 return !is_dark;
559 }
560 }
561 if let Some(rest) = cond.strip_prefix("prefers-reduced-motion:") {
564 let pref = rest.trim();
565 return pref == "no-preference";
566 }
567 if let Some(rest) = cond.strip_prefix("prefers-contrast:") {
573 let pref = rest.trim();
574 return pref == "no-preference";
575 }
576 if let Some(rest) = cond.strip_prefix("any-pointer:") {
583 return rest.trim() == "fine";
584 }
585 if let Some(rest) = cond.strip_prefix("pointer:") {
586 return rest.trim() == "fine";
587 }
588 if let Some(rest) = cond.strip_prefix("any-hover:") {
589 return rest.trim() == "hover";
590 }
591 if let Some(rest) = cond.strip_prefix("hover:") {
592 return rest.trim() == "hover";
593 }
594 if let Some(rest) = cond.strip_prefix("scripting:") {
601 return rest.trim() == "enabled";
602 }
603 if let Some(rest) = cond.strip_prefix("forced-colors:") {
604 return rest.trim() == "none";
605 }
606 if let Some(rest) = cond.strip_prefix("inverted-colors:") {
607 return rest.trim() == "none";
608 }
609 if let Some(rest) = cond.strip_prefix("color-gamut:") {
610 return rest.trim() == "srgb";
611 }
612 if let Some(rest) = cond.strip_prefix("update:") {
620 return rest.trim() == "fast";
621 }
622 if let Some(rest) = cond.strip_prefix("grid:") {
623 return rest.trim() == "0";
624 }
625 if let Some(rest) = cond.strip_prefix("prefers-reduced-transparency:") {
626 return rest.trim() == "no-preference";
627 }
628 if let Some(rest) = cond.strip_prefix("prefers-reduced-data:") {
629 return rest.trim() == "no-preference";
630 }
631 true }
633
634pub(crate) fn parse_css_declarations(css: &str) -> Vec<Rule> {
636 let mut rules = Vec::new();
637 let blocks = split_css_blocks(css);
638 for (selector_part, decl_part) in blocks {
639 let selector_part = selector_part.trim();
640 let decl_part = decl_part.trim();
641 if selector_part.is_empty() || decl_part.is_empty() {
642 continue;
643 }
644 if selector_part.starts_with('@') {
645 continue;
646 }
647 let selectors: Vec<Selector> = split_top_level_commas(selector_part)
650 .into_iter()
651 .filter_map(|s| parse_selector(s.trim()))
652 .collect();
653 if selectors.is_empty() {
654 continue;
655 }
656 let decl_part = match decl_part.find('{') {
660 Some(pos) => decl_part.get(..pos).unwrap_or(decl_part).trim(),
661 None => decl_part,
662 };
663 let declarations: Vec<Declaration> = decl_part
664 .split(';')
665 .filter_map(|decl| {
666 let mut kv = decl.splitn(2, ':');
667 let name = kv.next()?.trim();
668 let raw_value = kv.next()?.trim();
669 let (value, important) = match raw_value.rfind('!') {
670 Some(pos)
671 if raw_value
672 .get(pos + 1..)
673 .unwrap_or("")
674 .trim()
675 .eq_ignore_ascii_case("important") =>
676 {
677 (raw_value.get(..pos).unwrap_or("").trim(), true)
678 }
679 _ => (raw_value, false),
680 };
681 if name.is_empty() || value.is_empty() {
682 return None;
683 }
684 Some(Declaration {
685 name: String::from(name),
686 value: String::from(value),
687 important,
688 })
689 })
690 .collect();
691 if !declarations.is_empty() {
692 rules.push(Rule {
693 selectors,
694 declarations,
695 layer: None,
696 media_query: None,
697 container_query: None,
698 });
699 }
700 }
701 rules
702}
703
704pub(crate) fn split_css_blocks(css: &str) -> Vec<(String, String)> {
705 let mut blocks = Vec::new();
706 let mut depth = 0;
707 let mut selector_start = 0;
708 let mut block_start = 0;
709 let bytes = css.as_bytes();
710
711 for i in 0..bytes.len() {
712 match bytes[i] {
713 b'{' => {
714 if depth == 0 {
715 block_start = i + 1;
716 }
717 depth += 1;
718 }
719 b'}' => {
720 depth -= 1;
721 if depth <= 0 {
722 depth = 0;
723 if let (Some(selector), Some(body)) = (
724 css.get(selector_start..block_start.saturating_sub(1)),
725 css.get(block_start..i),
726 ) {
727 blocks.push((selector.to_string(), body.to_string()));
728 }
729 selector_start = i + 1;
730 }
731 }
732 _ => {}
733 }
734 }
735
736 blocks
737}
738
739