1use alloc::{collections::BTreeMap, string::ToString, vec::Vec};
14use core::{fmt, num::ParseIntError};
15
16pub use azul_simplecss::Error as SimplecssError;
17use azul_simplecss::Tokenizer;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[repr(C)]
22pub struct CssSyntaxErrorPos {
23 pub row: usize,
24 pub col: usize,
25}
26
27impl From<azul_simplecss::ErrorPos> for CssSyntaxErrorPos {
28 fn from(p: azul_simplecss::ErrorPos) -> Self {
29 Self {
30 row: p.row,
31 col: p.col,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38#[repr(C)]
39pub struct CssSyntaxInvalidAdvance {
40 pub expected: isize,
41 pub total: usize,
42 pub pos: CssSyntaxErrorPos,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[repr(C, u8)]
48pub enum CssSyntaxError {
49 UnexpectedEndOfStream(CssSyntaxErrorPos),
50 InvalidAdvance(CssSyntaxInvalidAdvance),
51 UnsupportedToken(CssSyntaxErrorPos),
52 UnknownToken(CssSyntaxErrorPos),
53}
54
55impl From<SimplecssError> for CssSyntaxError {
56 fn from(e: SimplecssError) -> Self {
57 match e {
58 SimplecssError::UnexpectedEndOfStream(pos) => Self::UnexpectedEndOfStream(pos.into()),
59 SimplecssError::InvalidAdvance {
60 expected,
61 total,
62 pos,
63 } => Self::InvalidAdvance(CssSyntaxInvalidAdvance {
64 expected,
65 total,
66 pos: pos.into(),
67 }),
68 SimplecssError::UnsupportedToken(pos) => Self::UnsupportedToken(pos.into()),
69 SimplecssError::UnknownToken(pos) => Self::UnknownToken(pos.into()),
70 }
71 }
72}
73
74pub use crate::props::property::CssParsingError;
75use crate::{
76 corety::{AzString, OptionString},
77 css::{
78 AttributeMatchOp, Css, CssAttributeSelector, CssDeclaration, CssNthChildSelector, CssPath,
79 CssPathPseudoSelector, CssPathSelector, CssRuleBlock, DynamicCssProperty, NodeTypeTag,
80 NodeTypeTagParseError, NodeTypeTagParseErrorOwned,
81 },
82 dynamic_selector::{
83 parse_os_version, BoolCondition, DynamicSelector, DynamicSelectorVec, EnvVariable,
84 LanguageCondition, MediaType, MinMaxRange, OrientationType, OsCondition, ThemeCondition,
85 },
86 props::{
87 basic::parse::parse_parentheses,
88 property::{
89 parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
90 CssParsingErrorOwned, CssProperty, CssPropertyType,
91 },
92 },
93};
94
95#[derive(Debug, Clone, PartialEq)]
97pub struct CssParseError<'a> {
98 pub css_string: &'a str,
99 pub error: CssParseErrorInner<'a>,
100 pub location: ErrorLocationRange,
101}
102
103#[derive(Debug, Clone, PartialEq)]
105#[repr(C)]
106pub struct CssParseErrorOwned {
107 pub css_string: AzString,
108 pub error: CssParseErrorInnerOwned,
109 pub location: ErrorLocationRange,
110}
111
112impl CssParseError<'_> {
113 #[must_use]
114 pub fn to_contained(&self) -> CssParseErrorOwned {
115 CssParseErrorOwned {
116 css_string: self.css_string.to_string().into(),
117 error: self.error.to_contained(),
118 location: self.location,
119 }
120 }
121}
122
123impl CssParseErrorOwned {
124 #[must_use]
125 pub fn to_shared(&self) -> CssParseError<'_> {
126 CssParseError {
127 css_string: self.css_string.as_str(),
128 error: self.error.to_shared(),
129 location: self.location,
130 }
131 }
132}
133
134#[must_use]
141fn clamp_to_char_boundary(s: &str, pos: usize) -> usize {
142 let mut pos = pos.min(s.len());
143 while pos > 0 && !s.is_char_boundary(pos) {
144 pos -= 1;
145 }
146 pos
147}
148
149impl<'a> CssParseError<'a> {
150 #[must_use]
152 pub fn get_error_string(&self) -> &'a str {
153 let (start, end) = (
154 self.location.start.original_pos,
155 self.location.end.original_pos,
156 );
157 let start = clamp_to_char_boundary(self.css_string, start);
162 let end = clamp_to_char_boundary(self.css_string, end);
163 let (start, end) = (start.min(end), start.max(end));
164 self.css_string[start..end].trim()
165 }
166}
167
168#[derive(Debug, Clone, PartialEq)]
169pub enum CssParseErrorInner<'a> {
170 ParseError(CssSyntaxError),
172 UnclosedBlock,
174 MalformedCss,
176 DynamicCssParseError(DynamicCssParseError<'a>),
179 PseudoSelectorParseError(CssPseudoSelectorParseError<'a>),
181 NodeTypeTag(NodeTypeTagParseError<'a>),
183 UnknownPropertyKey(&'a str, &'a str),
186 VarOnShorthandProperty {
192 key: CombinedCssPropertyType,
193 value: &'a str,
194 },
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
199#[repr(C)]
200pub struct UnknownPropertyKeyError {
201 pub key: AzString,
202 pub value: AzString,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
207#[repr(C)]
208pub struct VarOnShorthandPropertyError {
209 pub key: CombinedCssPropertyType,
210 pub value: AzString,
211}
212
213#[derive(Debug, Clone, PartialEq)]
214#[repr(C, u8)]
215pub enum CssParseErrorInnerOwned {
216 ParseError(CssSyntaxError),
217 UnclosedBlock,
218 MalformedCss,
219 DynamicCssParseError(DynamicCssParseErrorOwned),
220 PseudoSelectorParseError(CssPseudoSelectorParseErrorOwned),
221 NodeTypeTag(NodeTypeTagParseErrorOwned),
222 UnknownPropertyKey(UnknownPropertyKeyError),
223 VarOnShorthandProperty(VarOnShorthandPropertyError),
224}
225
226impl CssParseErrorInner<'_> {
227 #[must_use]
228 pub fn to_contained(&self) -> CssParseErrorInnerOwned {
229 match self {
230 CssParseErrorInner::ParseError(e) => CssParseErrorInnerOwned::ParseError(*e),
231 CssParseErrorInner::UnclosedBlock => CssParseErrorInnerOwned::UnclosedBlock,
232 CssParseErrorInner::MalformedCss => CssParseErrorInnerOwned::MalformedCss,
233 CssParseErrorInner::DynamicCssParseError(e) => {
234 CssParseErrorInnerOwned::DynamicCssParseError(e.to_contained())
235 }
236 CssParseErrorInner::PseudoSelectorParseError(e) => {
237 CssParseErrorInnerOwned::PseudoSelectorParseError(e.to_contained())
238 }
239 CssParseErrorInner::NodeTypeTag(e) => {
240 CssParseErrorInnerOwned::NodeTypeTag(e.to_contained())
241 }
242 CssParseErrorInner::UnknownPropertyKey(a, b) => {
243 CssParseErrorInnerOwned::UnknownPropertyKey(UnknownPropertyKeyError {
244 key: (*a).to_string().into(),
245 value: (*b).to_string().into(),
246 })
247 }
248 CssParseErrorInner::VarOnShorthandProperty { key, value } => {
249 CssParseErrorInnerOwned::VarOnShorthandProperty(VarOnShorthandPropertyError {
250 key: *key,
251 value: (*value).to_string().into(),
252 })
253 }
254 }
255 }
256}
257
258impl CssParseErrorInnerOwned {
259 #[must_use]
260 pub fn to_shared(&self) -> CssParseErrorInner<'_> {
261 match self {
262 Self::ParseError(e) => CssParseErrorInner::ParseError(*e),
263 Self::UnclosedBlock => CssParseErrorInner::UnclosedBlock,
264 Self::MalformedCss => CssParseErrorInner::MalformedCss,
265 Self::DynamicCssParseError(e) => {
266 CssParseErrorInner::DynamicCssParseError(e.to_shared())
267 }
268 Self::PseudoSelectorParseError(e) => {
269 CssParseErrorInner::PseudoSelectorParseError(e.to_shared())
270 }
271 Self::NodeTypeTag(e) => CssParseErrorInner::NodeTypeTag(e.to_shared()),
272 Self::UnknownPropertyKey(e) => {
273 CssParseErrorInner::UnknownPropertyKey(e.key.as_str(), e.value.as_str())
274 }
275 Self::VarOnShorthandProperty(e) => CssParseErrorInner::VarOnShorthandProperty {
276 key: e.key,
277 value: e.value.as_str(),
278 },
279 }
280 }
281}
282
283impl_display! { CssParseErrorInner<'a>, {
284 ParseError(e) => format!("Parse Error: {:?}", e),
285 UnclosedBlock => "Unclosed block",
286 MalformedCss => "Malformed Css",
287 DynamicCssParseError(e) => format!("{}", e),
288 PseudoSelectorParseError(e) => format!("Failed to parse pseudo-selector: {}", e),
289 NodeTypeTag(e) => format!("Failed to parse CSS selector path: {}", e),
290 UnknownPropertyKey(k, v) => format!("Unknown CSS key: \"{}: {}\"", k, v),
291 VarOnShorthandProperty { key, value } => format!(
292 "Error while parsing: \"{}: {};\": var() cannot be used on shorthand properties - use `{}-top` or `{}-x` as the key instead: ",
293 key, value, key, key
294 ),
295}}
296
297impl From<CssSyntaxError> for CssParseErrorInner<'_> {
298 fn from(e: CssSyntaxError) -> Self {
299 CssParseErrorInner::ParseError(e)
300 }
301}
302
303impl From<SimplecssError> for CssParseErrorInner<'_> {
304 fn from(e: SimplecssError) -> Self {
305 CssParseErrorInner::ParseError(CssSyntaxError::from(e))
306 }
307}
308
309impl_from! { DynamicCssParseError<'a>, CssParseErrorInner::DynamicCssParseError }
310impl_from! { NodeTypeTagParseError<'a>, CssParseErrorInner::NodeTypeTag }
311impl_from! { CssPseudoSelectorParseError<'a>, CssParseErrorInner::PseudoSelectorParseError }
312
313#[derive(Debug, Clone, PartialEq, Eq)]
314pub enum CssPseudoSelectorParseError<'a> {
315 EmptyNthChild,
316 UnknownSelector(&'a str, Option<&'a str>),
317 InvalidNthChildPattern(&'a str),
318 InvalidNthChild(ParseIntError),
319}
320
321impl From<ParseIntError> for CssPseudoSelectorParseError<'_> {
322 fn from(e: ParseIntError) -> Self {
323 CssPseudoSelectorParseError::InvalidNthChild(e)
324 }
325}
326
327impl_display! { CssPseudoSelectorParseError<'a>, {
328 EmptyNthChild => format!("\
329 Empty :nth-child() selector - nth-child() must at least take a number, \
330 a pattern (such as \"2n+3\") or the values \"even\" or \"odd\"."
331 ),
332 UnknownSelector(selector, value) => {
333 let format_str = value
334 .as_ref()
335 .map_or_else(|| (*selector).to_string(), |v| format!("{selector}({v})"));
336 format!("Invalid or unknown CSS pseudo-selector: ':{format_str}'")
337 },
338 InvalidNthChildPattern(selector) => format!(
339 "Invalid pseudo-selector :{} - value has to be a \
340 number, \"even\" or \"odd\" or a pattern such as \"2n+3\"", selector
341 ),
342 InvalidNthChild(e) => format!("Invalid :nth-child pseudo-selector: ':{}'", e),
343}}
344
345#[derive(Debug, Clone, PartialEq, Eq)]
347#[repr(C)]
348pub struct UnknownSelectorError {
349 pub selector: AzString,
350 pub suggestion: OptionString,
351}
352
353#[derive(Debug, Clone, PartialEq, Eq)]
354#[repr(C, u8)]
355pub enum CssPseudoSelectorParseErrorOwned {
356 EmptyNthChild,
357 UnknownSelector(UnknownSelectorError),
358 InvalidNthChildPattern(AzString),
359 InvalidNthChild(crate::props::basic::error::ParseIntError),
360}
361
362impl CssPseudoSelectorParseError<'_> {
363 #[must_use]
364 pub fn to_contained(&self) -> CssPseudoSelectorParseErrorOwned {
365 match self {
366 CssPseudoSelectorParseError::EmptyNthChild => {
367 CssPseudoSelectorParseErrorOwned::EmptyNthChild
368 }
369 CssPseudoSelectorParseError::UnknownSelector(a, b) => {
370 CssPseudoSelectorParseErrorOwned::UnknownSelector(UnknownSelectorError {
371 selector: (*a).to_string().into(),
372 suggestion: b.map(|s| AzString::from(s.to_string())).into(),
373 })
374 }
375 CssPseudoSelectorParseError::InvalidNthChildPattern(s) => {
376 CssPseudoSelectorParseErrorOwned::InvalidNthChildPattern((*s).to_string().into())
377 }
378 CssPseudoSelectorParseError::InvalidNthChild(e) => {
379 CssPseudoSelectorParseErrorOwned::InvalidNthChild(e.clone().into())
380 }
381 }
382 }
383}
384
385impl CssPseudoSelectorParseErrorOwned {
386 #[must_use]
387 pub fn to_shared(&self) -> CssPseudoSelectorParseError<'_> {
388 match self {
389 Self::EmptyNthChild => CssPseudoSelectorParseError::EmptyNthChild,
390 Self::UnknownSelector(e) => CssPseudoSelectorParseError::UnknownSelector(
391 e.selector.as_str(),
392 e.suggestion.as_ref().map(AzString::as_str),
393 ),
394 Self::InvalidNthChildPattern(s) => {
395 CssPseudoSelectorParseError::InvalidNthChildPattern(s)
396 }
397 Self::InvalidNthChild(e) => CssPseudoSelectorParseError::InvalidNthChild(e.to_std()),
398 }
399 }
400}
401
402#[derive(Debug, Clone, PartialEq)]
404pub enum DynamicCssParseError<'a> {
405 InvalidBraceContents(&'a str),
407 UnexpectedValue(CssParsingError<'a>),
409}
410
411impl_display! { DynamicCssParseError<'a>, {
412 InvalidBraceContents(e) => format!("Invalid contents of var()/env() function: ({})", e),
413 UnexpectedValue(e) => format!("{}", e),
414}}
415
416impl<'a> From<CssParsingError<'a>> for DynamicCssParseError<'a> {
417 fn from(e: CssParsingError<'a>) -> Self {
418 DynamicCssParseError::UnexpectedValue(e)
419 }
420}
421
422#[derive(Debug, Clone, PartialEq)]
423#[repr(C, u8)]
424pub enum DynamicCssParseErrorOwned {
425 InvalidBraceContents(AzString),
426 UnexpectedValue(CssParsingErrorOwned),
427}
428
429impl DynamicCssParseError<'_> {
430 #[must_use]
431 pub fn to_contained(&self) -> DynamicCssParseErrorOwned {
432 match self {
433 DynamicCssParseError::InvalidBraceContents(s) => {
434 DynamicCssParseErrorOwned::InvalidBraceContents((*s).to_string().into())
435 }
436 DynamicCssParseError::UnexpectedValue(e) => {
437 DynamicCssParseErrorOwned::UnexpectedValue(e.to_contained())
438 }
439 }
440 }
441}
442
443impl DynamicCssParseErrorOwned {
444 #[must_use]
445 pub fn to_shared(&self) -> DynamicCssParseError<'_> {
446 match self {
447 Self::InvalidBraceContents(s) => DynamicCssParseError::InvalidBraceContents(s),
448 Self::UnexpectedValue(e) => DynamicCssParseError::UnexpectedValue(e.to_shared()),
449 }
450 }
451}
452
453pub fn pseudo_selector_from_str<'a>(
459 selector: &'a str,
460 value: Option<&'a str>,
461) -> Result<CssPathPseudoSelector, CssPseudoSelectorParseError<'a>> {
462 match selector {
463 "first" => Ok(CssPathPseudoSelector::First),
464 "last" => Ok(CssPathPseudoSelector::Last),
465 "placeholder" | ":placeholder" => Ok(CssPathPseudoSelector::Placeholder),
470 "hover" => Ok(CssPathPseudoSelector::Hover),
471 "active" => Ok(CssPathPseudoSelector::Active),
472 "focus" => Ok(CssPathPseudoSelector::Focus),
473 "seat-focus" => Ok(CssPathPseudoSelector::SeatFocus),
474 "dragging" => Ok(CssPathPseudoSelector::Dragging),
475 "drag-over" => Ok(CssPathPseudoSelector::DragOver),
476 "root" => Ok(CssPathPseudoSelector::Root),
477 "nth-child" => {
478 let value = value.ok_or(CssPseudoSelectorParseError::EmptyNthChild)?;
479 let parsed = parse_nth_child_selector(value)?;
480 Ok(CssPathPseudoSelector::NthChild(parsed))
481 }
482 "lang" => {
483 let lang_value = value.ok_or(CssPseudoSelectorParseError::UnknownSelector(
484 selector, value,
485 ))?;
486 let lang_value = lang_value
488 .trim()
489 .trim_start_matches('"')
490 .trim_end_matches('"')
491 .trim_start_matches('\'')
492 .trim_end_matches('\'')
493 .trim();
494 Ok(CssPathPseudoSelector::Lang(AzString::from(
495 lang_value.to_string(),
496 )))
497 }
498 _ => Err(CssPseudoSelectorParseError::UnknownSelector(
499 selector, value,
500 )),
501 }
502}
503
504#[must_use]
508pub fn parse_attribute_selector(input: &str) -> Option<CssAttributeSelector> {
509 let s = input.trim();
510 if s.is_empty() {
511 return None;
512 }
513
514 let compound_ops: [(&str, AttributeMatchOp); 5] = [
517 ("~=", AttributeMatchOp::Includes),
518 ("|=", AttributeMatchOp::DashMatch),
519 ("^=", AttributeMatchOp::Prefix),
520 ("$=", AttributeMatchOp::Suffix),
521 ("*=", AttributeMatchOp::Substring),
522 ];
523 let (op, op_pos): (AttributeMatchOp, Option<usize>) = compound_ops
524 .iter()
525 .find_map(|(pat, op)| s.find(pat).map(|i| (*op, Some(i))))
526 .or_else(|| s.find('=').map(|i| (AttributeMatchOp::Eq, Some(i))))
527 .unwrap_or((AttributeMatchOp::Exists, None));
528
529 let (name, value) = match op_pos {
530 None => (s, None),
531 Some(i) => {
532 let name = s[..i].trim();
533 let op_len = if matches!(op, AttributeMatchOp::Eq) {
534 1
535 } else {
536 2
537 };
538 let raw_value = s[i + op_len..].trim();
539 let unquoted = strip_attribute_quotes(raw_value)?;
540 (name, Some(unquoted))
541 }
542 };
543
544 if name.is_empty() {
545 return None;
546 }
547 if name
549 .chars()
550 .any(|c| c.is_whitespace() || c == '"' || c == '\'')
551 {
552 return None;
553 }
554
555 Some(CssAttributeSelector {
556 name: name.to_string().into(),
557 op,
558 value: value.map_or_else(
559 || OptionString::None,
560 |v| OptionString::Some(v.to_string().into()),
561 ),
562 })
563}
564
565fn strip_attribute_quotes(s: &str) -> Option<&str> {
568 let bytes = s.as_bytes();
569 if bytes.len() >= 2 {
570 let first = bytes[0];
571 let last = bytes[bytes.len() - 1];
572 if (first == b'"' && last == b'"') || (first == b'\'' && last == b'\'') {
573 return Some(&s[1..s.len() - 1]);
574 }
575 if first == b'"' || first == b'\'' || last == b'"' || last == b'\'' {
576 return None;
578 }
579 } else if bytes.len() == 1 && (bytes[0] == b'"' || bytes[0] == b'\'') {
580 return None;
581 }
582 Some(s)
583}
584
585fn parse_nth_child_selector(
589 value: &str,
590) -> Result<CssNthChildSelector, CssPseudoSelectorParseError<'_>> {
591 let value = value.trim();
592
593 if value.is_empty() {
594 return Err(CssPseudoSelectorParseError::EmptyNthChild);
595 }
596
597 if let Ok(number) = value.parse::<u32>() {
598 return Ok(CssNthChildSelector::Number(number));
599 }
600
601 match value {
603 "even" => Ok(CssNthChildSelector::Even),
604 "odd" => Ok(CssNthChildSelector::Odd),
605 _ => parse_nth_child_pattern(value),
606 }
607}
608
609fn parse_nth_child_pattern(
611 value: &str,
612) -> Result<CssNthChildSelector, CssPseudoSelectorParseError<'_>> {
613 use crate::css::CssNthChildPattern;
614
615 let value = value.trim();
616
617 if value.is_empty() {
618 return Err(CssPseudoSelectorParseError::EmptyNthChild);
619 }
620
621 let repeat = value
623 .split('n')
624 .next()
625 .ok_or(CssPseudoSelectorParseError::InvalidNthChildPattern(value))?
626 .trim()
627 .parse::<u32>()?;
628
629 let mut offset_iterator = value.split('+');
631
632 offset_iterator.next().unwrap();
634
635 let offset = match offset_iterator.next() {
636 Some(offset_string) => {
637 let offset_string = offset_string.trim();
638 if offset_string.is_empty() {
639 return Err(CssPseudoSelectorParseError::InvalidNthChildPattern(value));
640 }
641 offset_string.parse::<u32>()?
642 }
643 None => 0,
644 };
645
646 Ok(CssNthChildSelector::Pattern(CssNthChildPattern {
647 pattern_repeat: repeat,
648 offset,
649 }))
650}
651
652#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
653#[repr(C)]
654pub struct ErrorLocation {
655 pub original_pos: usize,
656}
657
658#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
661#[repr(C)]
662pub struct ErrorLocationRange {
663 pub start: ErrorLocation,
664 pub end: ErrorLocation,
665}
666
667impl ErrorLocation {
668 #[must_use]
670 pub fn get_line_column_from_error(&self, css_string: &str) -> (usize, usize) {
671 let error_location =
677 clamp_to_char_boundary(css_string, self.original_pos.saturating_sub(1));
678 let (mut line_number, mut total_characters) = (0, 0);
679
680 for line in css_string[0..error_location].lines() {
681 line_number += 1;
682 total_characters += line.chars().count();
683 }
684
685 let total_characters = total_characters + line_number;
687 let column_pos = error_location - total_characters.saturating_sub(2);
688
689 (line_number, column_pos)
690 }
691}
692
693impl fmt::Display for CssParseError<'_> {
694 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
695 let start_location = self
696 .location
697 .start
698 .get_line_column_from_error(self.css_string);
699 let end_location = self
700 .location
701 .end
702 .get_line_column_from_error(self.css_string);
703 write!(
704 f,
705 " start: line {}:{}\r\n end: line {}:{}\r\n text: \"{}\"\r\n reason: {}",
706 start_location.0,
707 start_location.1,
708 end_location.0,
709 end_location.1,
710 self.get_error_string(),
711 self.error,
712 )
713 }
714}
715
716#[must_use]
722pub fn new_from_str(css_string: &str) -> (Css, Vec<CssParseWarnMsg<'_>>) {
723 let mut tokenizer = Tokenizer::new(css_string);
734 let mut keyframes: Vec<crate::css::Keyframes> = Vec::new();
735 let (rules, warnings) = new_from_str_inner(css_string, &mut tokenizer, &mut keyframes);
736 (
737 Css {
738 rules: rules.into(),
739 keyframes: keyframes.into(),
740 },
741 warnings,
742 )
743}
744
745fn stop_selector_permille(sel: &str) -> Option<u16> {
750 match sel {
751 "from" => Some(0),
752 "to" => Some(1000),
753 s => s.strip_suffix('%').and_then(|n| {
754 n.trim().parse::<f32>().ok().and_then(|pct| {
755 if (0.0..=100.0).contains(&pct) {
756 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
758 Some((pct * 10.0).round() as u16)
759 } else {
760 None
761 }
762 })
763 }),
764 }
765}
766
767fn get_error_location(tokenizer: &Tokenizer<'_>) -> ErrorLocation {
769 ErrorLocation {
770 original_pos: tokenizer.pos(),
771 }
772}
773
774#[derive(Debug, Clone, PartialEq, Eq)]
775pub enum CssPathParseError<'a> {
776 EmptyPath,
777 InvalidTokenEncountered(&'a str),
779 UnexpectedEndOfStream(&'a str),
780 SyntaxError(CssSyntaxError),
781 NodeTypeTag(NodeTypeTagParseError<'a>),
783 PseudoSelectorParseError(CssPseudoSelectorParseError<'a>),
785}
786
787impl_from! { NodeTypeTagParseError<'a>, CssPathParseError::NodeTypeTag }
788impl_from! { CssPseudoSelectorParseError<'a>, CssPathParseError::PseudoSelectorParseError }
789
790impl From<CssSyntaxError> for CssPathParseError<'_> {
791 fn from(e: CssSyntaxError) -> Self {
792 CssPathParseError::SyntaxError(e)
793 }
794}
795
796impl From<SimplecssError> for CssPathParseError<'_> {
797 fn from(e: SimplecssError) -> Self {
798 CssPathParseError::SyntaxError(CssSyntaxError::from(e))
799 }
800}
801
802#[derive(Debug, Clone, PartialEq, Eq)]
803pub enum CssPathParseErrorOwned {
804 EmptyPath,
805 InvalidTokenEncountered(AzString),
806 UnexpectedEndOfStream(AzString),
807 SyntaxError(CssSyntaxError),
808 NodeTypeTag(NodeTypeTagParseErrorOwned),
809 PseudoSelectorParseError(CssPseudoSelectorParseErrorOwned),
810}
811
812impl CssPathParseError<'_> {
813 #[must_use]
814 pub fn to_contained(&self) -> CssPathParseErrorOwned {
815 match self {
816 CssPathParseError::EmptyPath => CssPathParseErrorOwned::EmptyPath,
817 CssPathParseError::InvalidTokenEncountered(s) => {
818 CssPathParseErrorOwned::InvalidTokenEncountered((*s).to_string().into())
819 }
820 CssPathParseError::UnexpectedEndOfStream(s) => {
821 CssPathParseErrorOwned::UnexpectedEndOfStream((*s).to_string().into())
822 }
823 CssPathParseError::SyntaxError(e) => CssPathParseErrorOwned::SyntaxError(*e),
824 CssPathParseError::NodeTypeTag(e) => {
825 CssPathParseErrorOwned::NodeTypeTag(e.to_contained())
826 }
827 CssPathParseError::PseudoSelectorParseError(e) => {
828 CssPathParseErrorOwned::PseudoSelectorParseError(e.to_contained())
829 }
830 }
831 }
832}
833
834impl CssPathParseErrorOwned {
835 #[must_use]
836 pub fn to_shared(&self) -> CssPathParseError<'_> {
837 match self {
838 Self::EmptyPath => CssPathParseError::EmptyPath,
839 Self::InvalidTokenEncountered(s) => CssPathParseError::InvalidTokenEncountered(s),
840 Self::UnexpectedEndOfStream(s) => CssPathParseError::UnexpectedEndOfStream(s),
841 Self::SyntaxError(e) => CssPathParseError::SyntaxError(*e),
842 Self::NodeTypeTag(e) => CssPathParseError::NodeTypeTag(e.to_shared()),
843 Self::PseudoSelectorParseError(e) => {
844 CssPathParseError::PseudoSelectorParseError(e.to_shared())
845 }
846 }
847 }
848}
849
850pub fn parse_css_path(input: &str) -> Result<CssPath, CssPathParseError<'_>> {
880 use azul_simplecss::{Combinator, Token};
881
882 let input = input.trim();
883 if input.is_empty() {
884 return Err(CssPathParseError::EmptyPath);
885 }
886
887 let mut tokenizer = Tokenizer::new(input);
888 let mut selectors = Vec::new();
889
890 loop {
891 let token = tokenizer.parse_next()?;
892 match token {
893 Token::UniversalSelector => {
894 selectors.push(CssPathSelector::Global);
895 }
896 Token::TypeSelector(div_type) => match NodeTypeTag::from_str(div_type) {
897 Ok(nt) => selectors.push(CssPathSelector::Type(nt)),
902 Err(e) => return Err(CssPathParseError::NodeTypeTag(e)),
903 },
904 Token::IdSelector(id) => {
905 selectors.push(CssPathSelector::Id(id.to_string().into()));
906 }
907 Token::ClassSelector(class) => {
908 selectors.push(CssPathSelector::Class(class.to_string().into()));
909 }
910 Token::Combinator(Combinator::GreaterThan) => {
911 selectors.push(CssPathSelector::DirectChildren);
912 }
913 Token::Combinator(Combinator::Space) => {
914 selectors.push(CssPathSelector::Children);
915 }
916 Token::Combinator(Combinator::Plus) => {
917 selectors.push(CssPathSelector::AdjacentSibling);
918 }
919 Token::Combinator(Combinator::Tilde) => {
920 selectors.push(CssPathSelector::GeneralSibling);
921 }
922 Token::PseudoClass { selector, value } => {
923 selectors.push(CssPathSelector::PseudoSelector(pseudo_selector_from_str(
924 selector, value,
925 )?));
926 }
927 Token::EndOfStream => {
928 break;
929 }
930 _ => {
931 return Err(CssPathParseError::InvalidTokenEncountered(input));
932 }
933 }
934 }
935
936 if selectors.is_empty() {
937 Err(CssPathParseError::EmptyPath)
938 } else {
939 Ok(CssPath {
940 selectors: selectors.into(),
941 })
942 }
943}
944
945#[derive(Debug, Clone, PartialEq, Eq)]
946pub struct UnparsedCssRuleBlock<'a> {
947 pub path: CssPath,
949 pub declarations: BTreeMap<&'a str, (&'a str, ErrorLocationRange)>,
951 pub conditions: Vec<DynamicSelector>,
953}
954
955#[derive(Debug, Clone, PartialEq, Eq)]
957pub struct UnparsedCssRuleBlockOwned {
958 pub path: CssPath,
959 pub declarations: BTreeMap<String, (String, ErrorLocationRange)>,
960 pub conditions: Vec<DynamicSelector>,
961}
962
963impl UnparsedCssRuleBlock<'_> {
964 #[must_use]
965 pub fn to_contained(&self) -> UnparsedCssRuleBlockOwned {
966 UnparsedCssRuleBlockOwned {
967 path: self.path.clone(),
968 declarations: self
969 .declarations
970 .iter()
971 .map(|(k, (v, loc))| ((*k).to_string(), ((*v).to_string(), *loc)))
972 .collect(),
973 conditions: self.conditions.clone(),
974 }
975 }
976}
977
978impl UnparsedCssRuleBlockOwned {
979 #[must_use]
980 pub fn to_shared(&self) -> UnparsedCssRuleBlock<'_> {
981 UnparsedCssRuleBlock {
982 path: self.path.clone(),
983 declarations: self
984 .declarations
985 .iter()
986 .map(|(k, (v, loc))| (k.as_str(), (v.as_str(), *loc)))
987 .collect(),
988 conditions: self.conditions.clone(),
989 }
990 }
991}
992
993#[derive(Debug, Clone, PartialEq)]
994pub struct CssParseWarnMsg<'a> {
995 pub warning: CssParseWarnMsgInner<'a>,
996 pub location: ErrorLocationRange,
997}
998
999#[derive(Debug, Clone, PartialEq)]
1001pub struct CssParseWarnMsgOwned {
1002 pub warning: CssParseWarnMsgInnerOwned,
1003 pub location: ErrorLocationRange,
1004}
1005
1006impl CssParseWarnMsg<'_> {
1007 #[must_use]
1008 pub fn to_contained(&self) -> CssParseWarnMsgOwned {
1009 CssParseWarnMsgOwned {
1010 warning: self.warning.to_contained(),
1011 location: self.location,
1012 }
1013 }
1014}
1015
1016impl CssParseWarnMsgOwned {
1017 #[must_use]
1018 pub fn to_shared(&self) -> CssParseWarnMsg<'_> {
1019 CssParseWarnMsg {
1020 warning: self.warning.to_shared(),
1021 location: self.location,
1022 }
1023 }
1024}
1025
1026#[derive(Debug, Clone, PartialEq)]
1027pub enum CssParseWarnMsgInner<'a> {
1028 UnsupportedKeyValuePair { key: &'a str, value: &'a str },
1030 ParseError(CssParseErrorInner<'a>),
1032 SkippedRule {
1034 selector: Option<&'a str>,
1035 error: CssParseErrorInner<'a>,
1036 },
1037 SkippedDeclaration {
1039 key: &'a str,
1040 value: &'a str,
1041 error: CssParseErrorInner<'a>,
1042 },
1043 MalformedStructure { message: &'a str },
1045}
1046
1047#[derive(Debug, Clone, PartialEq)]
1048pub enum CssParseWarnMsgInnerOwned {
1049 UnsupportedKeyValuePair {
1050 key: String,
1051 value: String,
1052 },
1053 ParseError(CssParseErrorInnerOwned),
1054 SkippedRule {
1055 selector: Option<String>,
1056 error: CssParseErrorInnerOwned,
1057 },
1058 SkippedDeclaration {
1059 key: String,
1060 value: String,
1061 error: CssParseErrorInnerOwned,
1062 },
1063 MalformedStructure {
1064 message: String,
1065 },
1066}
1067
1068impl CssParseWarnMsgInner<'_> {
1069 #[must_use]
1070 pub fn to_contained(&self) -> CssParseWarnMsgInnerOwned {
1071 match self {
1072 Self::UnsupportedKeyValuePair { key, value } => {
1073 CssParseWarnMsgInnerOwned::UnsupportedKeyValuePair {
1074 key: (*key).to_string(),
1075 value: (*value).to_string(),
1076 }
1077 }
1078 Self::ParseError(e) => CssParseWarnMsgInnerOwned::ParseError(e.to_contained()),
1079 Self::SkippedRule { selector, error } => CssParseWarnMsgInnerOwned::SkippedRule {
1080 selector: selector.map(std::string::ToString::to_string),
1081 error: error.to_contained(),
1082 },
1083 Self::SkippedDeclaration { key, value, error } => {
1084 CssParseWarnMsgInnerOwned::SkippedDeclaration {
1085 key: (*key).to_string(),
1086 value: (*value).to_string(),
1087 error: error.to_contained(),
1088 }
1089 }
1090 Self::MalformedStructure { message } => CssParseWarnMsgInnerOwned::MalformedStructure {
1091 message: (*message).to_string(),
1092 },
1093 }
1094 }
1095}
1096
1097impl CssParseWarnMsgInnerOwned {
1098 #[must_use]
1099 pub fn to_shared(&self) -> CssParseWarnMsgInner<'_> {
1100 match self {
1101 Self::UnsupportedKeyValuePair { key, value } => {
1102 CssParseWarnMsgInner::UnsupportedKeyValuePair { key, value }
1103 }
1104 Self::ParseError(e) => CssParseWarnMsgInner::ParseError(e.to_shared()),
1105 Self::SkippedRule { selector, error } => CssParseWarnMsgInner::SkippedRule {
1106 selector: selector.as_deref(),
1107 error: error.to_shared(),
1108 },
1109 Self::SkippedDeclaration { key, value, error } => {
1110 CssParseWarnMsgInner::SkippedDeclaration {
1111 key,
1112 value,
1113 error: error.to_shared(),
1114 }
1115 }
1116 Self::MalformedStructure { message } => {
1117 CssParseWarnMsgInner::MalformedStructure { message }
1118 }
1119 }
1120 }
1121}
1122
1123impl_display! { CssParseWarnMsgInner<'a>, {
1124 UnsupportedKeyValuePair { key, value } => format!("Unsupported CSS property: \"{}: {}\"", key, value),
1125 ParseError(e) => format!("Parse error (recoverable): {}", e),
1126 SkippedRule { selector, error } => {
1127 let sel = selector.unwrap_or("unknown");
1128 format!("Skipped rule for selector '{sel}': {error}")
1129 },
1130 SkippedDeclaration { key, value, error } => format!("Skipped declaration '{}:{}': {}", key, value, error),
1131 MalformedStructure { message } => format!("Malformed CSS structure: {}", message),
1132}}
1133
1134fn parse_media_conditions(content: &str) -> Vec<DynamicSelector> {
1137 let mut conditions = Vec::new();
1138 let content = content.trim();
1139
1140 if content.eq_ignore_ascii_case("screen") {
1142 conditions.push(DynamicSelector::Media(MediaType::Screen));
1143 return conditions;
1144 }
1145 if content.eq_ignore_ascii_case("print") {
1146 conditions.push(DynamicSelector::Media(MediaType::Print));
1147 return conditions;
1148 }
1149 if content.eq_ignore_ascii_case("all") {
1150 conditions.push(DynamicSelector::Media(MediaType::All));
1151 return conditions;
1152 }
1153
1154 for part in content.split(" and ") {
1157 let part = part.trim();
1158
1159 if part.eq_ignore_ascii_case("screen")
1161 || part.eq_ignore_ascii_case("print")
1162 || part.eq_ignore_ascii_case("all")
1163 {
1164 if part.eq_ignore_ascii_case("screen") {
1165 conditions.push(DynamicSelector::Media(MediaType::Screen));
1166 } else if part.eq_ignore_ascii_case("print") {
1167 conditions.push(DynamicSelector::Media(MediaType::Print));
1168 } else if part.eq_ignore_ascii_case("all") {
1169 conditions.push(DynamicSelector::Media(MediaType::All));
1170 }
1171 continue;
1172 }
1173
1174 if let Some(inner) = part.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
1176 if let Some(selector) = parse_media_feature(inner) {
1177 conditions.push(selector);
1178 }
1179 }
1180 }
1181
1182 conditions
1183}
1184
1185fn parse_media_feature(feature: &str) -> Option<DynamicSelector> {
1187 let parts: Vec<&str> = feature.splitn(2, ':').collect();
1188 if parts.len() != 2 {
1189 return None;
1191 }
1192
1193 let key = parts[0].trim();
1194 let value = parts[1].trim();
1195
1196 match key.to_lowercase().as_str() {
1197 "min-width" => {
1198 if let Some(px) = parse_px_value(value) {
1199 return Some(DynamicSelector::ViewportWidth(MinMaxRange::new(
1200 Some(px),
1201 None,
1202 )));
1203 }
1204 }
1205 "max-width" => {
1206 if let Some(px) = parse_px_value(value) {
1207 return Some(DynamicSelector::ViewportWidth(MinMaxRange::new(
1208 None,
1209 Some(px),
1210 )));
1211 }
1212 }
1213 "min-height" => {
1214 if let Some(px) = parse_px_value(value) {
1215 return Some(DynamicSelector::ViewportHeight(MinMaxRange::new(
1216 Some(px),
1217 None,
1218 )));
1219 }
1220 }
1221 "max-height" => {
1222 if let Some(px) = parse_px_value(value) {
1223 return Some(DynamicSelector::ViewportHeight(MinMaxRange::new(
1224 None,
1225 Some(px),
1226 )));
1227 }
1228 }
1229 "orientation" => {
1230 if value.eq_ignore_ascii_case("portrait") {
1231 return Some(DynamicSelector::Orientation(OrientationType::Portrait));
1232 } else if value.eq_ignore_ascii_case("landscape") {
1233 return Some(DynamicSelector::Orientation(OrientationType::Landscape));
1234 }
1235 }
1236 "prefers-color-scheme" => {
1237 if value.eq_ignore_ascii_case("dark") {
1238 return Some(DynamicSelector::Theme(ThemeCondition::Dark));
1239 } else if value.eq_ignore_ascii_case("light") {
1240 return Some(DynamicSelector::Theme(ThemeCondition::Light));
1241 }
1242 }
1243 "prefers-reduced-motion" => {
1244 if value.eq_ignore_ascii_case("reduce") {
1245 return Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True));
1246 } else if value.eq_ignore_ascii_case("no-preference") {
1247 return Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False));
1248 }
1249 }
1250 "prefers-contrast" | "prefers-high-contrast" => {
1251 if value.eq_ignore_ascii_case("more")
1252 || value.eq_ignore_ascii_case("high")
1253 || value.eq_ignore_ascii_case("active")
1254 {
1255 return Some(DynamicSelector::PrefersHighContrast(BoolCondition::True));
1256 } else if value.eq_ignore_ascii_case("no-preference")
1257 || value.eq_ignore_ascii_case("none")
1258 {
1259 return Some(DynamicSelector::PrefersHighContrast(BoolCondition::False));
1260 }
1261 }
1262 "aspect-ratio" => {
1263 if let Some(ratio) = parse_ratio_value(value) {
1264 return Some(DynamicSelector::AspectRatio(MinMaxRange::new(
1265 Some(ratio),
1266 Some(ratio),
1267 )));
1268 }
1269 }
1270 "min-aspect-ratio" => {
1271 if let Some(ratio) = parse_ratio_value(value) {
1272 return Some(DynamicSelector::AspectRatio(MinMaxRange::new(
1273 Some(ratio),
1274 None,
1275 )));
1276 }
1277 }
1278 "max-aspect-ratio" => {
1279 if let Some(ratio) = parse_ratio_value(value) {
1280 return Some(DynamicSelector::AspectRatio(MinMaxRange::new(
1281 None,
1282 Some(ratio),
1283 )));
1284 }
1285 }
1286 _ => {}
1287 }
1288
1289 None
1290}
1291
1292fn parse_px_value(value: &str) -> Option<f32> {
1294 let value = value.trim();
1295 value
1296 .strip_suffix("px")
1297 .map_or_else(
1298 || value.parse::<f32>().ok(),
1300 |num_str| num_str.trim().parse::<f32>().ok(),
1301 )
1302 .filter(|v| v.is_finite())
1308}
1309
1310fn parse_ratio_value(value: &str) -> Option<f32> {
1312 let value = value.trim();
1313 if let Some((num, den)) = value.split_once('/') {
1314 let num: f32 = num.trim().parse().ok()?;
1315 let den: f32 = den.trim().parse().ok()?;
1316 if den == 0.0 {
1317 return None;
1318 }
1319 Some(num / den).filter(|r| r.is_finite())
1322 } else {
1323 value.parse::<f32>().ok().filter(|r| r.is_finite())
1324 }
1325}
1326
1327fn parse_container_conditions(content: &str) -> Vec<DynamicSelector> {
1330 let mut conditions = Vec::new();
1331 let content = content.trim();
1332
1333 let (name_part, query_part) = if content.starts_with('(') {
1336 (None, content)
1337 } else if let Some(paren_idx) = content.find('(') {
1338 let name = content[..paren_idx].trim();
1339 if name.is_empty() {
1340 (None, content)
1341 } else {
1342 (Some(name), &content[paren_idx..])
1343 }
1344 } else {
1345 if !content.is_empty() {
1347 conditions.push(DynamicSelector::ContainerName(AzString::from(
1348 content.to_string(),
1349 )));
1350 }
1351 return conditions;
1352 };
1353
1354 if let Some(name) = name_part {
1355 conditions.push(DynamicSelector::ContainerName(AzString::from(
1356 name.to_string(),
1357 )));
1358 }
1359
1360 for part in query_part.split(" and ") {
1362 let part = part.trim();
1363 if let Some(inner) = part.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
1364 if let Some(selector) = parse_container_feature(inner) {
1365 conditions.push(selector);
1366 }
1367 }
1368 }
1369
1370 conditions
1371}
1372
1373fn parse_container_feature(feature: &str) -> Option<DynamicSelector> {
1375 let (key, value) = feature.split_once(':')?;
1376 let key = key.trim();
1377 let value = value.trim();
1378
1379 match key.to_lowercase().as_str() {
1380 "min-width" => parse_px_value(value)
1381 .map(|px| DynamicSelector::ContainerWidth(MinMaxRange::new(Some(px), None))),
1382 "max-width" => parse_px_value(value)
1383 .map(|px| DynamicSelector::ContainerWidth(MinMaxRange::new(None, Some(px)))),
1384 "min-height" => parse_px_value(value)
1385 .map(|px| DynamicSelector::ContainerHeight(MinMaxRange::new(Some(px), None))),
1386 "max-height" => parse_px_value(value)
1387 .map(|px| DynamicSelector::ContainerHeight(MinMaxRange::new(None, Some(px)))),
1388 "aspect-ratio" => parse_ratio_value(value)
1389 .map(|r| DynamicSelector::AspectRatio(MinMaxRange::new(Some(r), Some(r)))),
1390 "min-aspect-ratio" => parse_ratio_value(value)
1391 .map(|r| DynamicSelector::AspectRatio(MinMaxRange::new(Some(r), None))),
1392 "max-aspect-ratio" => parse_ratio_value(value)
1393 .map(|r| DynamicSelector::AspectRatio(MinMaxRange::new(None, Some(r)))),
1394 _ => None,
1395 }
1396}
1397
1398fn parse_theme_condition(content: &str) -> Option<DynamicSelector> {
1401 let content = content.trim();
1402 let inner = content
1403 .strip_prefix('(')
1404 .and_then(|s| s.strip_suffix(')'))
1405 .unwrap_or(content)
1406 .trim();
1407 let inner = inner
1408 .strip_prefix('"')
1409 .and_then(|s| s.strip_suffix('"'))
1410 .or_else(|| inner.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1411 .unwrap_or(inner)
1412 .trim();
1413
1414 match inner.to_lowercase().as_str() {
1415 "dark" => Some(DynamicSelector::Theme(ThemeCondition::Dark)),
1416 "light" => Some(DynamicSelector::Theme(ThemeCondition::Light)),
1417 _ => None,
1418 }
1419}
1420
1421fn parse_lang_condition(content: &str) -> Option<DynamicSelector> {
1424 let content = content.trim();
1425
1426 let lang = content
1428 .strip_prefix('(')
1429 .and_then(|s| s.strip_suffix(')'))
1430 .unwrap_or(content)
1431 .trim();
1432
1433 let lang = lang
1434 .strip_prefix('"')
1435 .and_then(|s| s.strip_suffix('"'))
1436 .or_else(|| lang.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1437 .unwrap_or(lang)
1438 .trim();
1439
1440 if lang.is_empty() {
1441 return None;
1442 }
1443
1444 Some(DynamicSelector::Language(LanguageCondition::Prefix(
1446 AzString::from(lang.to_string()),
1447 )))
1448}
1449
1450const MAX_NESTING_DEPTH: usize = 1024;
1460
1461#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn new_from_str_inner<'a>(
1463 css_string: &'a str,
1464 tokenizer: &mut Tokenizer<'a>,
1465 keyframes_out: &mut Vec<crate::css::Keyframes>,
1466) -> (Vec<CssRuleBlock>, Vec<CssParseWarnMsg<'a>>) {
1467 use azul_simplecss::{Combinator, Token};
1468
1469 struct NestingLevel<'a> {
1472 paths: Vec<Vec<CssPathSelector>>,
1473 declarations: BTreeMap<&'a str, (&'a str, ErrorLocationRange)>,
1474 depth: usize,
1475 }
1476
1477 fn get_parent_paths(nesting_stack: &[NestingLevel<'_>]) -> Vec<Vec<CssPathSelector>> {
1479 nesting_stack
1480 .last()
1481 .map_or_else(Vec::new, |parent| parent.paths.clone())
1482 }
1483
1484 fn combine_paths(
1488 parent_paths: &[Vec<CssPathSelector>],
1489 child_path: &[CssPathSelector],
1490 is_pseudo_only: bool,
1491 ) -> Vec<Vec<CssPathSelector>> {
1492 if parent_paths.is_empty() {
1493 vec![child_path.to_vec()]
1494 } else {
1495 parent_paths
1496 .iter()
1497 .map(|parent| {
1498 let mut combined = parent.clone();
1499 if !is_pseudo_only && !child_path.is_empty() {
1500 combined.push(CssPathSelector::Children);
1502 }
1503 combined.extend(child_path.iter().cloned());
1504 combined
1505 })
1506 .collect()
1507 }
1508 }
1509
1510 struct KfCapture {
1515 name: String,
1516 stops: Vec<crate::css::KeyframeStop>,
1517 selectors: Vec<String>,
1518 props: Vec<crate::props::property::CssProperty>,
1519 in_stop: bool,
1520 }
1521
1522 let mut css_blocks = Vec::new();
1523 let mut warnings = Vec::new();
1524
1525 let mut block_nesting = 0_usize;
1526 let mut last_path: Vec<CssPathSelector> = Vec::new();
1527 let mut last_error_location = ErrorLocation { original_pos: 0 };
1528
1529 let mut at_rule_stack: Vec<(Vec<DynamicSelector>, usize)> = Vec::new();
1532 let mut pending_at_rule: Option<&str> = None;
1534 let mut pending_at_str_parts: Vec<String> = Vec::new();
1536 let mut keyframes_capture: Option<KfCapture> = None;
1537
1538 let mut nesting_stack: Vec<NestingLevel<'a>> = Vec::new();
1543 let mut current_paths: Vec<Vec<CssPathSelector>> = Vec::new();
1545 let mut current_declarations: BTreeMap<&str, (&str, ErrorLocationRange)> = BTreeMap::new();
1547
1548 let max_iterations = css_string.len().saturating_mul(10).max(1000);
1551 let mut iterations = 0_usize;
1552 let mut last_position = 0_usize;
1553 let mut stuck_count = 0_usize;
1554
1555 loop {
1556 iterations += 1;
1558 if iterations > max_iterations {
1559 warnings.push(CssParseWarnMsg {
1560 warning: CssParseWarnMsgInner::MalformedStructure {
1561 message: "Parser iteration limit exceeded - possible infinite loop",
1562 },
1563 location: ErrorLocationRange {
1564 start: last_error_location,
1565 end: get_error_location(tokenizer),
1566 },
1567 });
1568 break;
1569 }
1570
1571 let current_position = tokenizer.pos();
1573 if current_position == last_position {
1574 stuck_count += 1;
1575 if stuck_count > 10 {
1576 warnings.push(CssParseWarnMsg {
1577 warning: CssParseWarnMsgInner::MalformedStructure {
1578 message: "Parser stuck - position not advancing",
1579 },
1580 location: ErrorLocationRange {
1581 start: last_error_location,
1582 end: get_error_location(tokenizer),
1583 },
1584 });
1585 break;
1586 }
1587 } else {
1588 stuck_count = 0;
1589 last_position = current_position;
1590 }
1591
1592 let token = match tokenizer.parse_next() {
1593 Ok(token) => token,
1594 Err(e) => {
1595 let error_location = get_error_location(tokenizer);
1596 let warning = if block_nesting != 0 {
1602 CssParseWarnMsgInner::MalformedStructure {
1603 message: "Unclosed blocks at end of file",
1604 }
1605 } else {
1606 CssParseWarnMsgInner::ParseError(e.into())
1607 };
1608 warnings.push(CssParseWarnMsg {
1609 warning,
1610 location: ErrorLocationRange {
1611 start: last_error_location,
1612 end: error_location,
1613 },
1614 });
1615 break;
1617 }
1618 };
1619
1620 macro_rules! warn_and_continue {
1621 ($warning:expr) => {{
1622 warnings.push(CssParseWarnMsg {
1623 warning: $warning,
1624 location: ErrorLocationRange {
1625 start: last_error_location,
1626 end: get_error_location(tokenizer),
1627 },
1628 });
1629 continue;
1630 }};
1631 }
1632
1633 if keyframes_capture.is_some() {
1634 let mut close_kf = false;
1635 let mut eos = false;
1636 {
1637 let cap = keyframes_capture.as_mut().expect("checked is_some above");
1638 match token {
1639 Token::TypeSelector(sel) => {
1640 if !cap.in_stop {
1641 cap.selectors.push(sel.to_string());
1642 }
1643 }
1644 Token::BlockStart => {
1645 block_nesting += 1;
1646 cap.in_stop = true;
1647 cap.props.clear();
1648 }
1649 Token::Declaration(key, val) => {
1650 if cap.in_stop {
1651 let key_map = crate::props::property::get_css_key_map();
1652 if let Some(ty) = CssPropertyType::from_str(key.trim(), &key_map) {
1653 if let Ok(prop) = parse_css_property(ty, val.trim()) {
1654 cap.props.push(prop);
1655 }
1656 }
1657 }
1658 }
1659 Token::BlockEnd => {
1660 block_nesting = block_nesting.saturating_sub(1);
1661 if cap.in_stop {
1662 cap.in_stop = false;
1663 let props = core::mem::take(&mut cap.props);
1666 for sel in cap.selectors.drain(..) {
1667 if let Some(permille) = stop_selector_permille(&sel) {
1668 cap.stops.push(crate::css::KeyframeStop {
1669 permille,
1670 props: props.clone().into(),
1671 });
1672 }
1673 }
1674 } else {
1675 close_kf = true;
1676 }
1677 }
1678 Token::EndOfStream => {
1679 eos = true;
1680 }
1681 _ => {}
1684 }
1685 }
1686 if close_kf {
1687 let mut cap = keyframes_capture.take().expect("close_kf implies capture");
1688 cap.stops.sort_by_key(|st| st.permille);
1689 keyframes_out.push(crate::css::Keyframes {
1690 name: cap.name.into(),
1691 stops: cap.stops.into(),
1692 });
1693 }
1694 if eos {
1695 warnings.push(CssParseWarnMsg {
1696 warning: CssParseWarnMsgInner::MalformedStructure {
1697 message: "Unclosed blocks at end of file",
1698 },
1699 location: ErrorLocationRange {
1700 start: last_error_location,
1701 end: get_error_location(tokenizer),
1702 },
1703 });
1704 break;
1705 }
1706 last_error_location = get_error_location(tokenizer);
1707 continue;
1708 }
1709
1710 match token {
1711 Token::AtRule(rule_name) => {
1712 pending_at_rule = Some(rule_name);
1714 pending_at_str_parts.clear();
1715 }
1716 Token::AtStr(content) => {
1717 if pending_at_rule.is_some() {
1719 if !content.eq_ignore_ascii_case("and") {
1721 pending_at_str_parts.push(content.to_string());
1722 }
1723 }
1724 }
1725 Token::BlockStart => {
1726 if pending_at_rule.is_some_and(|r| r.eq_ignore_ascii_case("keyframes")) {
1731 pending_at_rule = None;
1732 let name = pending_at_str_parts.join(" ");
1733 pending_at_str_parts.clear();
1734 block_nesting += 1;
1735 keyframes_capture = Some(KfCapture {
1736 name,
1737 stops: Vec::new(),
1738 selectors: Vec::new(),
1739 props: Vec::new(),
1740 in_stop: false,
1741 });
1742 last_error_location = get_error_location(tokenizer);
1743 continue;
1744 }
1745 if let Some(rule_name) = pending_at_rule.take() {
1747 let combined_content = pending_at_str_parts.join(" and ");
1748 pending_at_str_parts.clear();
1749
1750 let conditions = match rule_name.to_lowercase().as_str() {
1751 "media" => parse_media_conditions(&combined_content),
1752 "lang" => parse_lang_condition(&combined_content)
1753 .into_iter()
1754 .collect(),
1755 "os" => {
1756 crate::dynamic_selector::parse_os_at_rule_content(&combined_content)
1757 .unwrap_or_default()
1758 }
1759 "theme" => parse_theme_condition(&combined_content)
1760 .into_iter()
1761 .collect(),
1762 "container" => parse_container_conditions(&combined_content),
1763 _ => {
1764 Vec::new()
1766 }
1767 };
1768
1769 if !conditions.is_empty() {
1770 at_rule_stack.push((conditions, block_nesting + 1));
1772 }
1773 }
1774
1775 block_nesting += 1;
1776
1777 if !current_paths.is_empty() || !last_path.is_empty() {
1779 if !last_path.is_empty() {
1781 current_paths.push(last_path.clone());
1782 last_path.clear();
1783 }
1784
1785 let combined_paths: Vec<Vec<CssPathSelector>> =
1789 if block_nesting > MAX_NESTING_DEPTH {
1790 std::mem::take(&mut current_paths)
1791 } else {
1792 let parent_paths = get_parent_paths(&nesting_stack);
1793 if parent_paths.is_empty() {
1794 current_paths.clone()
1795 } else {
1796 let mut result = Vec::new();
1798 for parent in &parent_paths {
1799 for child in ¤t_paths {
1800 let is_pseudo_only = child.first().is_some_and(|s| {
1802 matches!(s, CssPathSelector::PseudoSelector(_))
1803 });
1804 let mut combined = parent.clone();
1805 if !is_pseudo_only && !child.is_empty() {
1806 combined.push(CssPathSelector::Children);
1807 }
1808 combined.extend(child.iter().cloned());
1809 result.push(combined);
1810 }
1811 }
1812 result
1813 }
1814 };
1815
1816 nesting_stack.push(NestingLevel {
1818 paths: combined_paths,
1819 declarations: std::mem::take(&mut current_declarations),
1820 depth: block_nesting,
1821 });
1822 current_paths.clear();
1823 }
1824 }
1825 Token::Comma => {
1826 if !last_path.is_empty() {
1828 current_paths.push(last_path.clone());
1829 last_path.clear();
1830 }
1831 }
1832 Token::BlockEnd => {
1833 if block_nesting == 0 {
1834 warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1835 message: "Block end without matching block start"
1836 });
1837 }
1838
1839 let current_conditions: Vec<DynamicSelector> = at_rule_stack
1841 .iter()
1842 .flat_map(|(conds, _)| conds.iter().cloned())
1843 .collect();
1844
1845 while let Some((_, level)) = at_rule_stack.last() {
1847 if *level >= block_nesting {
1848 at_rule_stack.pop();
1849 } else {
1850 break;
1851 }
1852 }
1853
1854 block_nesting = block_nesting.saturating_sub(1);
1855
1856 if let Some(level) = nesting_stack.pop() {
1858 if !level.paths.is_empty() && !current_declarations.is_empty() {
1860 css_blocks.extend(level.paths.iter().map(|path| UnparsedCssRuleBlock {
1861 path: CssPath {
1862 selectors: path.clone().into(),
1863 },
1864 declarations: current_declarations.clone(),
1865 conditions: current_conditions.clone(),
1866 }));
1867 }
1868 current_declarations = level.declarations;
1870 }
1871
1872 last_path.clear();
1873 current_paths.clear();
1874 }
1875 Token::UniversalSelector => {
1876 last_path.push(CssPathSelector::Global);
1877 }
1878 Token::TypeSelector(div_type) => match NodeTypeTag::from_str(div_type) {
1879 Ok(nt) => last_path.push(CssPathSelector::Type(nt)),
1880 Err(e) => {
1881 warn_and_continue!(CssParseWarnMsgInner::SkippedRule {
1882 selector: Some(div_type),
1883 error: e.into(),
1884 });
1885 }
1886 },
1887 Token::IdSelector(id) => {
1888 last_path.push(CssPathSelector::Id(id.to_string().into()));
1889 }
1890 Token::ClassSelector(class) => {
1891 last_path.push(CssPathSelector::Class(class.to_string().into()));
1892 }
1893 Token::Combinator(Combinator::GreaterThan) => {
1894 last_path.push(CssPathSelector::DirectChildren);
1895 }
1896 Token::Combinator(Combinator::Space) => {
1897 last_path.push(CssPathSelector::Children);
1898 }
1899 Token::Combinator(Combinator::Plus) => {
1900 last_path.push(CssPathSelector::AdjacentSibling);
1901 }
1902 Token::Combinator(Combinator::Tilde) => {
1903 last_path.push(CssPathSelector::GeneralSibling);
1904 }
1905 Token::PseudoClass { selector, value }
1906 | Token::DoublePseudoClass { selector, value } => {
1907 match pseudo_selector_from_str(selector, value) {
1908 Ok(ps) => last_path.push(CssPathSelector::PseudoSelector(ps)),
1909 Err(e) => {
1910 warn_and_continue!(CssParseWarnMsgInner::SkippedRule {
1911 selector: Some(selector),
1912 error: e.into(),
1913 });
1914 }
1915 }
1916 }
1917 Token::AttributeSelector(attr) => {
1918 if let Some(sel) = parse_attribute_selector(attr) {
1919 last_path.push(CssPathSelector::Attribute(sel));
1920 } else {
1921 warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1922 message: "Malformed attribute selector, rule skipped",
1923 })
1924 }
1925 }
1926 Token::Declaration(key, val) => {
1927 current_declarations.insert(
1928 key,
1929 (
1930 val,
1931 ErrorLocationRange {
1932 start: last_error_location,
1933 end: get_error_location(tokenizer),
1934 },
1935 ),
1936 );
1937 }
1938 Token::EndOfStream => {
1939 if block_nesting != 0 {
1940 warnings.push(CssParseWarnMsg {
1941 warning: CssParseWarnMsgInner::MalformedStructure {
1942 message: "Unclosed blocks at end of file",
1943 },
1944 location: ErrorLocationRange {
1945 start: last_error_location,
1946 end: get_error_location(tokenizer),
1947 },
1948 });
1949 }
1950 break;
1951 }
1952 _ => { }
1953 }
1954
1955 last_error_location = get_error_location(tokenizer);
1956 }
1957
1958 let (stylesheet, mut block_warnings) = css_blocks_to_stylesheet(css_blocks, css_string);
1960 warnings.append(&mut block_warnings);
1961
1962 (stylesheet, warnings)
1963}
1964
1965fn resolve_var_reference(
1974 decl: CssDeclaration,
1975 custom_props: &BTreeMap<String, String>,
1976) -> CssDeclaration {
1977 let CssDeclaration::Dynamic(dyn_prop) = decl else {
1978 return decl;
1979 };
1980 if EnvVariable::from_dynamic_id(dyn_prop.dynamic_id.as_str()).is_some() {
1983 return CssDeclaration::Dynamic(dyn_prop);
1984 }
1985 let name = dyn_prop.dynamic_id.as_str().trim_start_matches("--");
1987 if let Some(raw) = custom_props.get(name) {
1988 if let Ok(parsed) = parse_css_property(dyn_prop.default_value.get_type(), raw) {
1989 return CssDeclaration::Static(parsed);
1990 }
1991 }
1992 CssDeclaration::Static(dyn_prop.default_value)
1993}
1994
1995fn css_blocks_to_stylesheet<'a>(
1996 css_blocks: Vec<UnparsedCssRuleBlock<'a>>,
1997 css_string: &'a str,
1998) -> (Vec<CssRuleBlock>, Vec<CssParseWarnMsg<'a>>) {
1999 let css_key_map = crate::props::property::get_css_key_map();
2000 let mut warnings = Vec::new();
2001 let mut parsed_css_blocks = Vec::new();
2002
2003 let mut custom_props: BTreeMap<String, String> = BTreeMap::new();
2012 for block in &css_blocks {
2013 for (key, (value, _)) in &block.declarations {
2014 if let Some(name) = key.strip_prefix("--") {
2015 custom_props.insert(name.to_string(), value.trim().to_string());
2016 }
2017 }
2018 }
2019
2020 for unparsed_css_block in css_blocks {
2021 let mut declarations = Vec::<CssDeclaration>::new();
2022
2023 for (unparsed_css_key, (unparsed_css_value, location)) in &unparsed_css_block.declarations {
2024 if unparsed_css_key.starts_with("--") {
2027 continue;
2028 }
2029 match parse_declaration_resilient(
2030 unparsed_css_key,
2031 unparsed_css_value,
2032 *location,
2033 &css_key_map,
2034 ) {
2035 Ok(decls) => {
2036 declarations.extend(
2037 decls
2038 .into_iter()
2039 .map(|d| resolve_var_reference(d, &custom_props)),
2040 );
2041 }
2042 Err(e) => {
2043 warnings.push(CssParseWarnMsg {
2044 warning: CssParseWarnMsgInner::SkippedDeclaration {
2045 key: unparsed_css_key,
2046 value: unparsed_css_value,
2047 error: e,
2048 },
2049 location: *location,
2050 });
2051 }
2052 }
2053 }
2054
2055 parsed_css_blocks.push(CssRuleBlock {
2056 path: unparsed_css_block.path,
2057 declarations: declarations.into(),
2058 conditions: unparsed_css_block.conditions.into(),
2059 priority: crate::css::rule_priority::AUTHOR,
2060 });
2061 }
2062
2063 (parsed_css_blocks, warnings)
2064}
2065
2066fn parse_declaration_resilient<'a>(
2067 unparsed_css_key: &'a str,
2068 unparsed_css_value: &'a str,
2069 location: ErrorLocationRange,
2070 css_key_map: &CssKeyMap,
2071) -> Result<Vec<CssDeclaration>, CssParseErrorInner<'a>> {
2072 let mut declarations = Vec::new();
2073
2074 if let Some(combined_key) = CombinedCssPropertyType::from_str(unparsed_css_key, css_key_map) {
2075 if let Some(env) = check_if_value_is_css_env(unparsed_css_value) {
2079 let (env_var, fallback) = env?;
2080 match parse_combined_css_property(combined_key, fallback) {
2081 Ok(parsed_props) => {
2082 declarations.extend(
2083 parsed_props
2084 .into_iter()
2085 .map(|p| env_declaration(env_var, p)),
2086 );
2087 }
2088 Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
2089 }
2090 return Ok(declarations);
2091 }
2092 if check_if_value_is_css_var(unparsed_css_value).is_some() {
2093 return Err(CssParseErrorInner::VarOnShorthandProperty {
2094 key: combined_key,
2095 value: unparsed_css_value,
2096 });
2097 }
2098
2099 match parse_combined_css_property(combined_key, unparsed_css_value) {
2101 Ok(parsed_props) => {
2102 declarations.extend(parsed_props.into_iter().map(CssDeclaration::Static));
2103 }
2104 Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
2105 }
2106 } else if let Some(normal_key) = CssPropertyType::from_str(unparsed_css_key, css_key_map) {
2107 if let Some(env) = check_if_value_is_css_env(unparsed_css_value) {
2108 let (env_var, fallback) = env?;
2109 match parse_css_property(normal_key, fallback) {
2110 Ok(parsed_fallback) => declarations.push(env_declaration(env_var, parsed_fallback)),
2111 Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
2112 }
2113 } else if let Some(css_var) = check_if_value_is_css_var(unparsed_css_value) {
2114 let (css_var_id, css_var_default) = css_var?;
2115 match parse_css_property(normal_key, css_var_default) {
2116 Ok(parsed_default) => {
2117 declarations.push(CssDeclaration::Dynamic(DynamicCssProperty {
2118 dynamic_id: css_var_id.to_string().into(),
2119 default_value: parsed_default,
2120 }));
2121 }
2122 Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
2123 }
2124 } else {
2125 match parse_css_property(normal_key, unparsed_css_value) {
2126 Ok(parsed_value) => {
2127 declarations.push(CssDeclaration::Static(parsed_value));
2128 }
2129 Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
2130 }
2131 }
2132 } else {
2133 return Err(CssParseErrorInner::UnknownPropertyKey(
2134 unparsed_css_key,
2135 unparsed_css_value,
2136 ));
2137 }
2138
2139 Ok(declarations)
2140}
2141
2142pub fn parse_css_declaration<'a>(
2151 unparsed_css_key: &'a str,
2152 unparsed_css_value: &'a str,
2153 location: ErrorLocationRange,
2154 css_key_map: &CssKeyMap,
2155 warnings: &mut Vec<CssParseWarnMsg<'a>>,
2156 declarations: &mut Vec<CssDeclaration>,
2157) -> Result<(), CssParseErrorInner<'a>> {
2158 match parse_declaration_resilient(unparsed_css_key, unparsed_css_value, location, css_key_map) {
2159 Ok(mut decls) => {
2160 declarations.append(&mut decls);
2161 Ok(())
2162 }
2163 Err(e) => {
2164 if let CssParseErrorInner::UnknownPropertyKey(key, val) = &e {
2165 warnings.push(CssParseWarnMsg {
2166 warning: CssParseWarnMsgInner::UnsupportedKeyValuePair { key, value: val },
2167 location,
2168 });
2169 Ok(()) } else {
2171 Err(e) }
2173 }
2174 }
2175}
2176
2177fn env_declaration(env_var: Option<EnvVariable>, fallback: CssProperty) -> CssDeclaration {
2182 match env_var {
2183 Some(v) => CssDeclaration::Dynamic(DynamicCssProperty {
2184 dynamic_id: v.dynamic_id(),
2185 default_value: fallback,
2186 }),
2187 None => CssDeclaration::Static(fallback),
2188 }
2189}
2190
2191fn check_if_value_is_css_env(
2204 unparsed_css_value: &str,
2205) -> Option<Result<(Option<EnvVariable>, &str), CssParseErrorInner<'_>>> {
2206 const KNOWN_NAME_DEFAULT: &str = "0px";
2207
2208 let (_, brace_contents) = parse_parentheses(unparsed_css_value, &["env"]).ok()?;
2209
2210 let mut parts = brace_contents.splitn(2, ',');
2211 let name = parts.next().unwrap_or("").trim();
2212 let fallback = parts.next().map(str::trim).filter(|f| !f.is_empty());
2213 let env_var = EnvVariable::from_css_name(name);
2214
2215 Some(match (env_var, fallback) {
2216 (Some(v), Some(f)) => Ok((Some(v), f)),
2217 (Some(v), None) => Ok((Some(v), KNOWN_NAME_DEFAULT)),
2218 (None, Some(f)) => Ok((None, f)),
2219 (None, None) => Err(DynamicCssParseError::InvalidBraceContents(brace_contents).into()),
2220 })
2221}
2222
2223fn check_if_value_is_css_var(
2224 unparsed_css_value: &str,
2225) -> Option<Result<(&str, &str), CssParseErrorInner<'_>>> {
2226 const DEFAULT_VARIABLE_DEFAULT: &str = "none";
2227
2228 let (_, brace_contents) = parse_parentheses(unparsed_css_value, &["var"]).ok()?;
2229
2230 Some(match parse_css_variable_brace_contents(brace_contents) {
2232 Some((variable_id, default_value)) => Ok((
2233 variable_id,
2234 default_value.unwrap_or(DEFAULT_VARIABLE_DEFAULT),
2235 )),
2236 None => Err(DynamicCssParseError::InvalidBraceContents(brace_contents).into()),
2237 })
2238}
2239
2240fn parse_css_variable_brace_contents(input: &str) -> Option<(&str, Option<&str>)> {
2247 let input = input.trim();
2248
2249 let mut split_comma_iter = input.splitn(2, ',');
2250 let var_name = split_comma_iter.next()?;
2251 let var_name = var_name.trim();
2252
2253 if !var_name.starts_with("--") {
2254 return None; }
2256
2257 Some((&var_name[2..], split_comma_iter.next()))
2258}
2259
2260#[cfg(test)]
2261#[allow(
2262 clippy::all,
2263 clippy::pedantic,
2264 clippy::nursery,
2265 unused_qualifications,
2266 single_use_lifetimes
2267)]
2268mod autotest_generated {
2269 use super::*;
2270 use crate::css::CssNthChildPattern;
2271
2272 fn catch<R>(f: impl FnOnce() -> R) -> Result<R, String> {
2281 std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_err(|e| {
2282 e.downcast_ref::<String>().cloned().unwrap_or_else(|| {
2283 e.downcast_ref::<&str>().map_or_else(
2284 || "<non-string panic payload>".to_string(),
2285 |s| (*s).to_string(),
2286 )
2287 })
2288 })
2289 }
2290
2291 fn key_map() -> CssKeyMap {
2292 crate::props::property::get_css_key_map()
2293 }
2294
2295 fn loc(start: usize, end: usize) -> ErrorLocationRange {
2296 ErrorLocationRange {
2297 start: ErrorLocation {
2298 original_pos: start,
2299 },
2300 end: ErrorLocation { original_pos: end },
2301 }
2302 }
2303
2304 const HOSTILE: &[&str] = &[
2306 "",
2307 " ",
2308 " \t\n\r ",
2309 "\0",
2310 "\u{1F600}",
2311 "e\u{301}\u{301}\u{301}",
2312 "-0",
2313 "0",
2314 "NaN",
2315 "inf",
2316 "-inf",
2317 "9223372036854775807",
2318 "-9223372036854775808",
2319 "18446744073709551616",
2320 "1e309",
2321 ";",
2322 "{}",
2323 "()",
2324 "((((",
2325 "))))",
2326 "\"",
2327 "'",
2328 "\\",
2329 "//",
2330 "/*",
2331 "valid;garbage",
2332 " valid ",
2333 "a=b=c",
2334 ":::",
2335 "--",
2336 ];
2337
2338 #[test]
2345 fn pseudo_selector_from_str_valid_minimal() {
2346 assert_eq!(
2347 pseudo_selector_from_str("hover", None),
2348 Ok(CssPathPseudoSelector::Hover)
2349 );
2350 assert_eq!(
2351 pseudo_selector_from_str("first", None),
2352 Ok(CssPathPseudoSelector::First)
2353 );
2354 assert_eq!(
2355 pseudo_selector_from_str("last", None),
2356 Ok(CssPathPseudoSelector::Last)
2357 );
2358 assert_eq!(
2359 pseudo_selector_from_str("active", None),
2360 Ok(CssPathPseudoSelector::Active)
2361 );
2362 assert_eq!(
2363 pseudo_selector_from_str("focus", None),
2364 Ok(CssPathPseudoSelector::Focus)
2365 );
2366 assert_eq!(
2367 pseudo_selector_from_str("seat-focus", None),
2368 Ok(CssPathPseudoSelector::SeatFocus)
2369 );
2370 assert_eq!(
2371 pseudo_selector_from_str("dragging", None),
2372 Ok(CssPathPseudoSelector::Dragging)
2373 );
2374 assert_eq!(
2375 pseudo_selector_from_str("drag-over", None),
2376 Ok(CssPathPseudoSelector::DragOver)
2377 );
2378 assert_eq!(
2379 pseudo_selector_from_str("root", None),
2380 Ok(CssPathPseudoSelector::Root)
2381 );
2382 }
2383
2384 #[test]
2385 fn pseudo_selector_from_str_nth_child_needs_a_value() {
2386 assert_eq!(
2387 pseudo_selector_from_str("nth-child", None),
2388 Err(CssPseudoSelectorParseError::EmptyNthChild)
2389 );
2390 assert_eq!(
2391 pseudo_selector_from_str("nth-child", Some("2")),
2392 Ok(CssPathPseudoSelector::NthChild(
2393 CssNthChildSelector::Number(2)
2394 ))
2395 );
2396 }
2397
2398 #[test]
2399 fn pseudo_selector_from_str_lang_strips_quotes() {
2400 for v in ["de-DE", "\"de-DE\"", "'de-DE'", " \"de-DE\" "] {
2402 assert_eq!(
2403 pseudo_selector_from_str("lang", Some(v)),
2404 Ok(CssPathPseudoSelector::Lang(AzString::from(
2405 "de-DE".to_string()
2406 ))),
2407 "lang value {v:?} did not normalise to de-DE"
2408 );
2409 }
2410 assert!(pseudo_selector_from_str("lang", None).is_err());
2412 }
2413
2414 #[test]
2415 fn pseudo_selector_from_str_empty_and_whitespace_are_rejected() {
2416 assert!(pseudo_selector_from_str("", None).is_err());
2417 assert!(pseudo_selector_from_str(" ", None).is_err());
2418 assert!(pseudo_selector_from_str("\t\n", None).is_err());
2419 assert!(pseudo_selector_from_str(" hover ", None).is_err());
2421 }
2422
2423 #[test]
2424 fn pseudo_selector_from_str_garbage_and_unicode_never_panic() {
2425 for s in HOSTILE {
2426 for v in [None, Some(*s), Some("2"), Some("\u{1F600}")] {
2427 let r = catch(|| pseudo_selector_from_str(s, v));
2428 assert!(
2429 r.is_ok(),
2430 "pseudo_selector_from_str({s:?}, {v:?}) panicked: {}",
2431 r.unwrap_err()
2432 );
2433 assert!(
2435 pseudo_selector_from_str(s, v).is_err(),
2436 "pseudo_selector_from_str({s:?}, {v:?}) unexpectedly succeeded"
2437 );
2438 }
2439 }
2440 }
2441
2442 #[test]
2443 fn pseudo_selector_from_str_extremely_long_input_terminates() {
2444 let long = "z".repeat(200_000);
2445 assert!(pseudo_selector_from_str(&long, None).is_err());
2446 assert_eq!(
2448 pseudo_selector_from_str("hover", Some(&long)),
2449 Ok(CssPathPseudoSelector::Hover)
2450 );
2451 assert!(pseudo_selector_from_str("nth-child", Some(&long)).is_err());
2453 }
2454
2455 #[test]
2456 fn pseudo_selector_from_str_deeply_nested_value_does_not_stack_overflow() {
2457 let nested = "(".repeat(10_000);
2458 let r = catch(|| pseudo_selector_from_str("nth-child", Some(&nested)).is_err());
2459 assert_eq!(
2460 r,
2461 Ok(true),
2462 "deeply nested nth-child value was not rejected safely"
2463 );
2464 }
2465
2466 #[test]
2469 fn parse_attribute_selector_valid_minimal() {
2470 let sel = parse_attribute_selector("href").expect("bare attribute name must parse");
2471 assert_eq!(sel.name.as_str(), "href");
2472 assert_eq!(sel.op, AttributeMatchOp::Exists);
2473 assert_eq!(sel.value.clone().into_option(), None);
2474 }
2475
2476 #[test]
2477 fn parse_attribute_selector_all_operators() {
2478 let cases: [(&str, AttributeMatchOp); 6] = [
2479 ("a=b", AttributeMatchOp::Eq),
2480 ("a~=b", AttributeMatchOp::Includes),
2481 ("a|=b", AttributeMatchOp::DashMatch),
2482 ("a^=b", AttributeMatchOp::Prefix),
2483 ("a$=b", AttributeMatchOp::Suffix),
2484 ("a*=b", AttributeMatchOp::Substring),
2485 ];
2486 for (input, expected_op) in cases {
2487 let sel =
2488 parse_attribute_selector(input).unwrap_or_else(|| panic!("{input:?} should parse"));
2489 assert_eq!(sel.name.as_str(), "a", "wrong name for {input:?}");
2490 assert_eq!(sel.op, expected_op, "wrong op for {input:?}");
2491 assert_eq!(
2492 sel.value
2493 .clone()
2494 .into_option()
2495 .map(|v| v.as_str().to_string()),
2496 Some("b".to_string()),
2497 "wrong value for {input:?}"
2498 );
2499 }
2500 }
2501
2502 #[test]
2503 fn parse_attribute_selector_quotes_are_stripped_and_unbalanced_rejected() {
2504 for input in ["a=\"b\"", "a='b'", "a=b", " a = \"b\" "] {
2505 let sel =
2506 parse_attribute_selector(input).unwrap_or_else(|| panic!("{input:?} should parse"));
2507 assert_eq!(
2508 sel.value
2509 .clone()
2510 .into_option()
2511 .map(|v| v.as_str().to_string()),
2512 Some("b".to_string()),
2513 "quotes not stripped for {input:?}"
2514 );
2515 }
2516 for input in ["a=\"b", "a=b\"", "a='b", "a=b'", "a=\"b'", "a=\"", "a='"] {
2518 assert!(
2519 parse_attribute_selector(input).is_none(),
2520 "unbalanced quote {input:?} should be rejected"
2521 );
2522 }
2523 }
2524
2525 #[test]
2526 fn parse_attribute_selector_empty_and_malformed_are_rejected() {
2527 for input in ["", " ", "\t\n", "=", "=b", " =b", "\"a\"", "'a'"] {
2528 assert!(
2529 parse_attribute_selector(input).is_none(),
2530 "{input:?} should be rejected (empty/quoted name)"
2531 );
2532 }
2533 assert!(parse_attribute_selector("a b").is_none());
2535 assert!(parse_attribute_selector("a b=c").is_none());
2536 }
2537
2538 #[test]
2539 fn parse_attribute_selector_unicode_name_is_accepted_and_does_not_panic() {
2540 let sel = parse_attribute_selector("data-\u{1F600}")
2541 .expect("a non-ASCII attribute name has no whitespace/quotes, so it is accepted");
2542 assert_eq!(sel.name.as_str(), "data-\u{1F600}");
2543
2544 let sel = parse_attribute_selector("lang=\"\u{4E2D}\u{6587}\"").expect("unicode value");
2546 assert_eq!(
2547 sel.value
2548 .clone()
2549 .into_option()
2550 .map(|v| v.as_str().to_string()),
2551 Some("\u{4E2D}\u{6587}".to_string())
2552 );
2553 }
2554
2555 #[test]
2559 fn parse_attribute_selector_result_invariants_hold_for_hostile_input() {
2560 let long = format!("a={}", "x".repeat(100_000));
2561 let nested = format!("a={}", "[".repeat(10_000));
2562 let mut inputs: Vec<&str> = HOSTILE.to_vec();
2563 inputs.push(&long);
2564 inputs.push(&nested);
2565 inputs.push("title=\"a~=b\"");
2566 inputs.push("a=b=c");
2567 inputs.push("[[[[]]]]");
2568
2569 for input in inputs {
2570 let parsed = match catch(|| parse_attribute_selector(input)) {
2571 Ok(p) => p,
2572 Err(msg) => panic!("parse_attribute_selector({input:?}) panicked: {msg}"),
2573 };
2574 if let Some(sel) = parsed {
2575 let name = sel.name.as_str();
2576 assert!(!name.is_empty(), "empty name accepted for {input:?}");
2577 assert!(
2578 !name
2579 .chars()
2580 .any(|c| c.is_whitespace() || c == '"' || c == '\''),
2581 "name {name:?} contains whitespace/quotes for input {input:?}"
2582 );
2583 }
2584 }
2585 }
2586
2587 #[test]
2590 fn strip_attribute_quotes_balanced_unquoted_and_unbalanced() {
2591 assert_eq!(strip_attribute_quotes("\"abc\""), Some("abc"));
2593 assert_eq!(strip_attribute_quotes("'abc'"), Some("abc"));
2594 assert_eq!(strip_attribute_quotes("\"\""), Some(""));
2595 assert_eq!(strip_attribute_quotes("''"), Some(""));
2596 assert_eq!(strip_attribute_quotes("abc"), Some("abc"));
2598 assert_eq!(strip_attribute_quotes(""), Some(""));
2599 assert_eq!(strip_attribute_quotes("a"), Some("a"));
2600 assert_eq!(strip_attribute_quotes("\"abc"), None);
2602 assert_eq!(strip_attribute_quotes("abc\""), None);
2603 assert_eq!(strip_attribute_quotes("'abc"), None);
2604 assert_eq!(strip_attribute_quotes("abc'"), None);
2605 assert_eq!(strip_attribute_quotes("\"abc'"), None);
2606 assert_eq!(strip_attribute_quotes("\""), None);
2607 assert_eq!(strip_attribute_quotes("'"), None);
2608 }
2609
2610 #[test]
2615 fn strip_attribute_quotes_multibyte_boundaries_never_panic() {
2616 let cases = [
2617 "\u{1F600}",
2618 "\"\u{1F600}\"",
2619 "'\u{4E2D}\u{6587}'",
2620 "\u{4E2D}\u{6587}",
2621 "\"\u{301}\"",
2622 "\u{301}",
2623 ];
2624 for s in cases {
2625 let r = catch(|| strip_attribute_quotes(s));
2626 assert!(
2627 r.is_ok(),
2628 "strip_attribute_quotes({s:?}) panicked: {}",
2629 r.unwrap_err()
2630 );
2631 }
2632 assert_eq!(strip_attribute_quotes("\"\u{1F600}\""), Some("\u{1F600}"));
2633 assert_eq!(strip_attribute_quotes("\u{1F600}"), Some("\u{1F600}"));
2634 }
2635
2636 #[test]
2637 fn strip_attribute_quotes_extremely_long_input_terminates() {
2638 let long = "x".repeat(500_000);
2639 assert_eq!(strip_attribute_quotes(&long), Some(long.as_str()));
2640 let quoted = format!("\"{long}\"");
2641 assert_eq!(strip_attribute_quotes("ed), Some(long.as_str()));
2642 }
2643
2644 #[test]
2647 fn parse_nth_child_selector_valid_minimal() {
2648 assert_eq!(
2649 parse_nth_child_selector("2"),
2650 Ok(CssNthChildSelector::Number(2))
2651 );
2652 assert_eq!(
2653 parse_nth_child_selector("0"),
2654 Ok(CssNthChildSelector::Number(0))
2655 );
2656 assert_eq!(
2657 parse_nth_child_selector("even"),
2658 Ok(CssNthChildSelector::Even)
2659 );
2660 assert_eq!(
2661 parse_nth_child_selector("odd"),
2662 Ok(CssNthChildSelector::Odd)
2663 );
2664 assert_eq!(
2665 parse_nth_child_selector(" 7 "),
2666 Ok(CssNthChildSelector::Number(7))
2667 );
2668 assert_eq!(
2669 parse_nth_child_selector("2n+3"),
2670 Ok(CssNthChildSelector::Pattern(CssNthChildPattern {
2671 pattern_repeat: 2,
2672 offset: 3
2673 }))
2674 );
2675 assert_eq!(
2676 parse_nth_child_selector("2n"),
2677 Ok(CssNthChildSelector::Pattern(CssNthChildPattern {
2678 pattern_repeat: 2,
2679 offset: 0
2680 }))
2681 );
2682 }
2683
2684 #[test]
2685 fn parse_nth_child_selector_empty_is_empty_nth_child_error() {
2686 assert_eq!(
2687 parse_nth_child_selector(""),
2688 Err(CssPseudoSelectorParseError::EmptyNthChild)
2689 );
2690 assert_eq!(
2691 parse_nth_child_selector(" \t\n "),
2692 Err(CssPseudoSelectorParseError::EmptyNthChild)
2693 );
2694 assert_eq!(
2695 parse_nth_child_pattern(""),
2696 Err(CssPseudoSelectorParseError::EmptyNthChild)
2697 );
2698 }
2699
2700 #[test]
2703 fn parse_nth_child_selector_numeric_limits_saturate_into_errors() {
2704 assert_eq!(
2705 parse_nth_child_selector("4294967295"),
2706 Ok(CssNthChildSelector::Number(u32::MAX))
2707 );
2708 for overflow in [
2709 "4294967296",
2710 "18446744073709551616",
2711 "99999999999999999999999999",
2712 "-1",
2713 "-0",
2714 ] {
2715 assert!(
2716 parse_nth_child_selector(overflow).is_err(),
2717 "{overflow:?} must not parse as an nth-child index"
2718 );
2719 }
2720 let huge = "9".repeat(100_000);
2722 assert!(parse_nth_child_selector(&huge).is_err());
2723 let huge_repeat = format!("{}n+1", "9".repeat(100_000));
2724 assert!(parse_nth_child_pattern(&huge_repeat).is_err());
2725 }
2726
2727 #[test]
2728 fn parse_nth_child_selector_float_and_non_finite_strings_are_rejected() {
2729 for v in ["NaN", "inf", "-inf", "1.5", "1e5", "0x2", "+2", " 2 n "] {
2730 let r = catch(|| parse_nth_child_selector(v));
2731 assert!(
2732 r.is_ok(),
2733 "parse_nth_child_selector({v:?}) panicked: {}",
2734 r.unwrap_err()
2735 );
2736 }
2737 assert!(parse_nth_child_selector("NaN").is_err());
2738 assert!(parse_nth_child_selector("inf").is_err());
2739 assert!(parse_nth_child_selector("1.5").is_err());
2740 }
2741
2742 #[test]
2743 fn parse_nth_child_pattern_malformed_offsets_are_rejected() {
2744 assert_eq!(
2746 parse_nth_child_pattern("2n+"),
2747 Err(CssPseudoSelectorParseError::InvalidNthChildPattern("2n+"))
2748 );
2749 assert!(parse_nth_child_pattern("2n+ ").is_err());
2750 assert!(parse_nth_child_pattern("2n+x").is_err());
2751 assert!(parse_nth_child_pattern("xn+1").is_err());
2752 for s in HOSTILE {
2755 let r = catch(|| parse_nth_child_pattern(s));
2756 assert!(
2757 r.is_ok(),
2758 "parse_nth_child_pattern({s:?}) panicked: {}",
2759 r.unwrap_err()
2760 );
2761 }
2762 }
2763
2764 #[test]
2765 fn parse_nth_child_selector_unicode_never_panics() {
2766 for v in [
2767 "\u{1F600}",
2768 "\u{FF12}",
2769 "2\u{301}",
2770 "e\u{301}ven",
2771 "\u{4E2D}n+\u{6587}",
2772 ] {
2773 let r = catch(|| parse_nth_child_selector(v));
2774 assert!(
2775 r.is_ok(),
2776 "parse_nth_child_selector({v:?}) panicked: {}",
2777 r.unwrap_err()
2778 );
2779 assert!(
2780 parse_nth_child_selector(v).is_err(),
2781 "{v:?} is not a valid nth-child value"
2782 );
2783 }
2784 }
2785
2786 #[test]
2789 fn parse_css_path_valid_minimal() {
2790 assert_eq!(
2792 parse_css_path("* div #my_id > .class:nth-child(2)"),
2793 Ok(CssPath {
2794 selectors: vec![
2795 CssPathSelector::Global,
2796 CssPathSelector::Type(NodeTypeTag::from_str("div").unwrap()),
2797 CssPathSelector::Children,
2798 CssPathSelector::Id("my_id".to_string().into()),
2799 CssPathSelector::DirectChildren,
2800 CssPathSelector::Class("class".to_string().into()),
2801 CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
2802 CssNthChildSelector::Number(2)
2803 )),
2804 ]
2805 .into()
2806 })
2807 );
2808 assert_eq!(
2809 parse_css_path("div"),
2810 Ok(CssPath {
2811 selectors: vec![CssPathSelector::Type(NodeTypeTag::from_str("div").unwrap())]
2812 .into()
2813 })
2814 );
2815 }
2816
2817 #[test]
2818 fn parse_css_path_empty_and_whitespace_are_empty_path_errors() {
2819 assert_eq!(parse_css_path(""), Err(CssPathParseError::EmptyPath));
2820 assert_eq!(parse_css_path(" "), Err(CssPathParseError::EmptyPath));
2821 assert_eq!(parse_css_path("\t\r\n "), Err(CssPathParseError::EmptyPath));
2822 assert!(matches!(
2826 parse_css_path("definitelynotatag"),
2827 Err(CssPathParseError::NodeTypeTag(_))
2828 ));
2829 }
2830
2831 #[test]
2832 fn parse_css_path_garbage_and_unicode_never_panic() {
2833 let long = "div ".repeat(50_000);
2834 let brackets = "[".repeat(10_000);
2835 let braces = "{".repeat(10_000);
2836 let mut inputs: Vec<&str> = HOSTILE.to_vec();
2837 inputs.push(&long);
2838 inputs.push(&brackets);
2839 inputs.push(&braces);
2840 inputs.push("div;garbage");
2841 inputs.push("div }");
2842 inputs.push(":::::");
2843 inputs.push("\u{1F600} > \u{4E2D}\u{6587}");
2844
2845 for input in inputs {
2846 let r = catch(|| parse_css_path(input));
2847 assert!(
2848 r.is_ok(),
2849 "parse_css_path({:.40?}) panicked: {}",
2850 input,
2851 r.unwrap_err()
2852 );
2853 }
2854 }
2855
2856 #[test]
2857 fn parse_css_path_rejects_block_tokens() {
2858 for input in ["div { }", "div {", "}"] {
2860 assert!(
2861 parse_css_path(input).is_err(),
2862 "{input:?} contains block tokens and must not parse as a path"
2863 );
2864 }
2865 }
2866
2867 #[test]
2868 fn parse_css_path_unknown_pseudo_selector_is_an_error() {
2869 assert!(parse_css_path("div:definitelynotapseudo").is_err());
2870 assert!(parse_css_path(".x:nth-child(notanumber)").is_err());
2871 }
2872
2873 #[test]
2881 fn parse_css_path_unknown_type_tag_is_not_silently_dropped() {
2882 let parsed = parse_css_path("div definitelynotatag");
2883 if let Ok(path) = &parsed {
2884 let selectors = path.selectors.as_slice();
2885 assert!(
2886 !matches!(
2887 selectors.last(),
2888 Some(
2889 CssPathSelector::Children
2890 | CssPathSelector::DirectChildren
2891 | CssPathSelector::AdjacentSibling
2892 | CssPathSelector::GeneralSibling
2893 )
2894 ),
2895 "BUG: the unknown type tag was dropped, leaving a dangling combinator; \
2896 `div definitelynotatag` now matches every descendant of div. \
2897 selectors = {selectors:?}"
2898 );
2899 }
2900 }
2901
2902 #[test]
2905 fn parse_media_conditions_valid_minimal() {
2906 assert_eq!(
2907 parse_media_conditions("screen"),
2908 vec![DynamicSelector::Media(MediaType::Screen)]
2909 );
2910 assert_eq!(
2911 parse_media_conditions("PRINT"),
2912 vec![DynamicSelector::Media(MediaType::Print)]
2913 );
2914 assert_eq!(
2915 parse_media_conditions("all"),
2916 vec![DynamicSelector::Media(MediaType::All)]
2917 );
2918 }
2919
2920 #[test]
2921 fn parse_media_conditions_parenthesised_and_compound() {
2922 let conds = parse_media_conditions("(min-width: 800px)");
2923 assert_eq!(conds.len(), 1);
2924 match &conds[0] {
2925 DynamicSelector::ViewportWidth(r) => {
2926 assert_eq!(r.min(), Some(800.0));
2927 assert_eq!(r.max(), None);
2928 }
2929 other => panic!("expected ViewportWidth, got {other:?}"),
2930 }
2931
2932 let conds = parse_media_conditions("screen and (max-width: 600px)");
2933 assert_eq!(
2934 conds.len(),
2935 2,
2936 "compound query should yield both conditions"
2937 );
2938 assert_eq!(conds[0], DynamicSelector::Media(MediaType::Screen));
2939 match &conds[1] {
2940 DynamicSelector::ViewportWidth(r) => {
2941 assert_eq!(r.min(), None);
2942 assert_eq!(r.max(), Some(600.0));
2943 }
2944 other => panic!("expected ViewportWidth, got {other:?}"),
2945 }
2946 }
2947
2948 #[test]
2949 fn parse_media_conditions_empty_and_garbage_yield_no_conditions() {
2950 for input in [
2951 "",
2952 " ",
2953 "((((",
2954 "))))",
2955 "\u{1F600}",
2956 "and",
2957 "(",
2958 ")",
2959 "()",
2960 ] {
2961 let r = catch(|| parse_media_conditions(input));
2962 match r {
2963 Ok(conds) => assert!(
2964 conds.is_empty(),
2965 "{input:?} should not produce media conditions, got {conds:?}"
2966 ),
2967 Err(msg) => panic!("parse_media_conditions({input:?}) panicked: {msg}"),
2968 }
2969 }
2970 }
2971
2972 #[test]
2973 fn parse_media_conditions_extremely_long_and_deeply_nested_terminate() {
2974 let nested = format!("({})", "(".repeat(10_000));
2975 let r = catch(|| parse_media_conditions(&nested));
2976 assert!(
2977 r.is_ok(),
2978 "deeply nested media query panicked: {}",
2979 r.unwrap_err()
2980 );
2981
2982 let long = "screen and ".repeat(20_000);
2983 let r = catch(|| parse_media_conditions(&long));
2984 assert!(
2985 r.is_ok(),
2986 "very long media query panicked: {}",
2987 r.unwrap_err()
2988 );
2989 }
2990
2991 #[test]
2992 fn parse_media_feature_known_features() {
2993 assert_eq!(
2994 parse_media_feature("orientation: portrait"),
2995 Some(DynamicSelector::Orientation(OrientationType::Portrait))
2996 );
2997 assert_eq!(
2998 parse_media_feature("orientation: LANDSCAPE"),
2999 Some(DynamicSelector::Orientation(OrientationType::Landscape))
3000 );
3001 assert_eq!(
3002 parse_media_feature("prefers-color-scheme: dark"),
3003 Some(DynamicSelector::Theme(ThemeCondition::Dark))
3004 );
3005 assert_eq!(
3006 parse_media_feature("prefers-reduced-motion: reduce"),
3007 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
3008 );
3009 assert_eq!(
3010 parse_media_feature("prefers-contrast: more"),
3011 Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
3012 );
3013 assert!(parse_media_feature("MIN-WIDTH: 800px").is_some());
3015 }
3016
3017 #[test]
3018 fn parse_media_feature_malformed_returns_none() {
3019 for input in [
3020 "",
3021 " ",
3022 "nocolon",
3023 "min-width:",
3024 "min-width: ",
3025 "min-width: abc",
3026 ": 800px",
3027 "unknown-feature: 800px",
3028 "orientation: sideways",
3029 "\u{1F600}: \u{1F600}",
3030 ] {
3031 let r = catch(|| parse_media_feature(input));
3032 match r {
3033 Ok(v) => assert!(v.is_none(), "{input:?} should be rejected, got {v:?}"),
3034 Err(msg) => panic!("parse_media_feature({input:?}) panicked: {msg}"),
3035 }
3036 }
3037 }
3038
3039 #[test]
3045 fn parse_media_feature_nan_width_does_not_erase_the_constraint() {
3046 for feature in ["min-width: NaN", "min-width: NaNpx", "max-width: nan"] {
3047 match parse_media_feature(feature) {
3048 None => {} Some(DynamicSelector::ViewportWidth(r)) => {
3050 assert!(
3051 r.min().is_some() || r.max().is_some(),
3052 "BUG: {feature:?} parsed into a ViewportWidth with no bounds at all \
3053 (the NaN collided with MinMaxRange's `absent` sentinel), so the \
3054 media query silently matches every viewport"
3055 );
3056 }
3057 Some(other) => panic!("unexpected selector for {feature:?}: {other:?}"),
3058 }
3059 }
3060 }
3061
3062 #[test]
3065 fn parse_px_value_valid_minimal() {
3066 assert_eq!(parse_px_value("800px"), Some(800.0));
3067 assert_eq!(parse_px_value("800"), Some(800.0));
3068 assert_eq!(parse_px_value(" 800px "), Some(800.0));
3069 assert_eq!(parse_px_value("0"), Some(0.0));
3070 assert_eq!(parse_px_value("1.5px"), Some(1.5));
3071 assert_eq!(parse_px_value("-10px"), Some(-10.0));
3072 }
3073
3074 #[test]
3075 fn parse_px_value_malformed_returns_none() {
3076 for input in [
3077 "",
3078 " ",
3079 "px",
3080 "abc",
3081 "8 0 0",
3082 "800pxx",
3083 "\u{1F600}",
3084 "800%",
3085 "--",
3086 ] {
3087 let r = catch(|| parse_px_value(input));
3088 match r {
3089 Ok(v) => assert!(v.is_none(), "{input:?} should be rejected, got {v:?}"),
3090 Err(msg) => panic!("parse_px_value({input:?}) panicked: {msg}"),
3091 }
3092 }
3093 }
3094
3095 #[test]
3098 fn parse_px_value_overflow_and_underflow_saturate_without_panicking() {
3099 assert_eq!(parse_px_value("-0"), Some(-0.0));
3100 assert_eq!(parse_px_value("1e-50"), Some(0.0));
3101 assert_eq!(parse_px_value("3.4e38px"), Some(3.4e38));
3102
3103 assert_eq!(parse_px_value("1e39px"), None);
3109
3110 let huge_digits = "9".repeat(100_000);
3111 let r = catch(|| parse_px_value(&huge_digits));
3112 assert!(
3113 r.is_ok(),
3114 "a 100k-digit number panicked: {}",
3115 r.unwrap_err()
3116 );
3117 }
3118
3119 #[test]
3125 fn parse_px_value_rejects_non_finite_values() {
3126 for input in [
3127 "NaN", "nan", "inf", "-inf", "infinity", "NaNpx", "infpx", "-infpx",
3128 ] {
3129 if let Some(px) = parse_px_value(input) {
3130 assert!(
3131 px.is_finite(),
3132 "BUG: parse_px_value({input:?}) returned the non-finite value {px}; \
3133 a non-finite length is not valid CSS and collides with MinMaxRange's \
3134 NaN `absent` sentinel"
3135 );
3136 }
3137 }
3138 }
3139
3140 #[test]
3143 fn parse_ratio_value_valid_minimal() {
3144 let r = parse_ratio_value("16/9").expect("16/9 should parse");
3145 assert!((r - (16.0 / 9.0)).abs() < 1e-6, "16/9 parsed as {r}");
3146 let r = parse_ratio_value("1.777").expect("bare float should parse");
3147 assert!((r - 1.777).abs() < 1e-6);
3148 let r = parse_ratio_value(" 16 / 9 ").expect("whitespace should be trimmed");
3149 assert!((r - (16.0 / 9.0)).abs() < 1e-6);
3150 }
3151
3152 #[test]
3153 fn parse_ratio_value_division_by_zero_is_rejected() {
3154 assert_eq!(parse_ratio_value("1/0"), None);
3156 assert_eq!(parse_ratio_value("1/-0"), None);
3157 assert_eq!(parse_ratio_value("0/0"), None);
3158 assert_eq!(parse_ratio_value("16/0.0"), None);
3159 }
3160
3161 #[test]
3162 fn parse_ratio_value_malformed_returns_none() {
3163 for input in [
3164 "",
3165 " ",
3166 "/",
3167 "16/",
3168 "/9",
3169 "a/b",
3170 "1/2/3",
3171 "\u{1F600}",
3172 "16:9",
3173 ] {
3174 let r = catch(|| parse_ratio_value(input));
3175 match r {
3176 Ok(v) => assert!(v.is_none(), "{input:?} should be rejected, got {v:?}"),
3177 Err(msg) => panic!("parse_ratio_value({input:?}) panicked: {msg}"),
3178 }
3179 }
3180 }
3181
3182 #[test]
3187 fn parse_ratio_value_never_returns_nan() {
3188 for input in ["NaN", "inf/inf", "NaN/1", "1/NaN", "-inf/inf", "inf/-inf"] {
3189 if let Some(r) = parse_ratio_value(input) {
3190 assert!(
3191 !r.is_nan(),
3192 "BUG: parse_ratio_value({input:?}) returned NaN, which MinMaxRange \
3193 reads back as `no bound` -- the aspect-ratio query silently matches \
3194 everything"
3195 );
3196 }
3197 }
3198 }
3199
3200 #[test]
3201 fn parse_ratio_value_extremely_long_input_terminates() {
3202 let huge = format!("{}/{}", "9".repeat(50_000), "9".repeat(50_000));
3203 let r = catch(|| parse_ratio_value(&huge));
3204 assert!(r.is_ok(), "huge ratio panicked: {}", r.unwrap_err());
3205 }
3206
3207 #[test]
3210 fn parse_container_conditions_valid_minimal() {
3211 assert_eq!(
3213 parse_container_conditions("sidebar"),
3214 vec![DynamicSelector::ContainerName(AzString::from(
3215 "sidebar".to_string()
3216 ))]
3217 );
3218
3219 let conds = parse_container_conditions("(min-width: 400px)");
3221 assert_eq!(conds.len(), 1);
3222 match &conds[0] {
3223 DynamicSelector::ContainerWidth(r) => {
3224 assert_eq!(r.min(), Some(400.0));
3225 assert_eq!(r.max(), None);
3226 }
3227 other => panic!("expected ContainerWidth, got {other:?}"),
3228 }
3229
3230 let conds = parse_container_conditions("sidebar (min-width: 400px)");
3232 assert_eq!(conds.len(), 2);
3233 assert_eq!(
3234 conds[0],
3235 DynamicSelector::ContainerName(AzString::from("sidebar".to_string()))
3236 );
3237 assert!(matches!(conds[1], DynamicSelector::ContainerWidth(_)));
3238 }
3239
3240 #[test]
3241 fn parse_container_conditions_empty_and_garbage_never_panic() {
3242 assert!(parse_container_conditions("").is_empty());
3243 assert!(parse_container_conditions(" ").is_empty());
3244
3245 let nested = "(".repeat(10_000);
3246 let long = "a".repeat(200_000);
3247 let mut inputs: Vec<&str> = HOSTILE.to_vec();
3248 inputs.push(&nested);
3249 inputs.push(&long);
3250
3251 for input in inputs {
3252 let r = catch(|| parse_container_conditions(input));
3253 assert!(
3254 r.is_ok(),
3255 "parse_container_conditions({:.40?}) panicked: {}",
3256 input,
3257 r.unwrap_err()
3258 );
3259 }
3260 }
3261
3262 #[test]
3263 fn parse_container_feature_malformed_returns_none() {
3264 for input in [
3265 "",
3266 " ",
3267 "nocolon",
3268 "min-width:",
3269 "min-width: abc",
3270 "unknown: 1px",
3271 ] {
3272 let r = catch(|| parse_container_feature(input));
3273 match r {
3274 Ok(v) => assert!(v.is_none(), "{input:?} should be rejected, got {v:?}"),
3275 Err(msg) => panic!("parse_container_feature({input:?}) panicked: {msg}"),
3276 }
3277 }
3278 assert!(parse_container_feature("min-height: 400px").is_some());
3279 assert!(parse_container_feature("MAX-WIDTH: 400px").is_some());
3280 }
3281
3282 #[test]
3285 fn parse_theme_condition_valid_minimal() {
3286 for input in [
3287 "dark",
3288 "(dark)",
3289 "DARK",
3290 "\"dark\"",
3291 "'dark'",
3292 "(\"dark\")",
3293 " dark ",
3294 ] {
3295 assert_eq!(
3296 parse_theme_condition(input),
3297 Some(DynamicSelector::Theme(ThemeCondition::Dark)),
3298 "theme {input:?} should resolve to Dark"
3299 );
3300 }
3301 assert_eq!(
3302 parse_theme_condition("light"),
3303 Some(DynamicSelector::Theme(ThemeCondition::Light))
3304 );
3305 }
3306
3307 #[test]
3308 fn parse_theme_condition_garbage_returns_none() {
3309 for input in [
3310 "",
3311 " ",
3312 "(",
3313 ")",
3314 "()",
3315 "sepia",
3316 "\u{1F600}",
3317 "dark light",
3318 "\"dark",
3319 ] {
3320 let r = catch(|| parse_theme_condition(input));
3321 match r {
3322 Ok(v) => assert!(v.is_none(), "theme {input:?} should be rejected, got {v:?}"),
3323 Err(msg) => panic!("parse_theme_condition({input:?}) panicked: {msg}"),
3324 }
3325 }
3326 }
3327
3328 #[test]
3329 fn parse_lang_condition_valid_minimal() {
3330 for input in ["de-DE", "(de-DE)", "(\"de-DE\")", "('de-DE')", " de-DE "] {
3331 assert_eq!(
3332 parse_lang_condition(input),
3333 Some(DynamicSelector::Language(LanguageCondition::Prefix(
3334 AzString::from("de-DE".to_string())
3335 ))),
3336 "lang {input:?} should resolve to Prefix(de-DE)"
3337 );
3338 }
3339 }
3340
3341 #[test]
3342 fn parse_lang_condition_empty_returns_none() {
3343 for input in ["", " ", "()", "( )", "( )"] {
3344 assert_eq!(
3345 parse_lang_condition(input),
3346 None,
3347 "empty lang {input:?} must not produce a condition"
3348 );
3349 }
3350 }
3351
3352 #[test]
3353 fn parse_lang_condition_unicode_and_long_input_never_panic() {
3354 let long = "a".repeat(200_000);
3355 let mut inputs: Vec<&str> = HOSTILE.to_vec();
3356 inputs.push(&long);
3357 for input in inputs {
3358 let r = catch(|| parse_lang_condition(input));
3359 assert!(
3360 r.is_ok(),
3361 "parse_lang_condition({:.40?}) panicked: {}",
3362 input,
3363 r.unwrap_err()
3364 );
3365 }
3366 }
3367
3368 #[test]
3371 fn parse_css_variable_brace_contents_valid_minimal() {
3372 assert_eq!(
3373 parse_css_variable_brace_contents("--main-bg-col"),
3374 Some(("main-bg-col", None))
3375 );
3376 let (name, default) = parse_css_variable_brace_contents("--main-bg-col, blue")
3377 .expect("var with default should parse");
3378 assert_eq!(name, "main-bg-col");
3379 assert_eq!(default.map(str::trim), Some("blue"));
3382 }
3383
3384 #[test]
3385 fn parse_css_variable_brace_contents_rejects_non_variables() {
3386 for input in [
3387 "",
3388 " ",
3389 "main-bg-col",
3390 "-main-bg-col",
3391 "blue",
3392 "\u{1F600}",
3393 ",",
3394 ] {
3395 assert_eq!(
3396 parse_css_variable_brace_contents(input),
3397 None,
3398 "{input:?} is not a `--` prefixed CSS variable"
3399 );
3400 }
3401 }
3402
3403 #[test]
3407 fn parse_css_variable_brace_contents_multibyte_name_does_not_split_a_char() {
3408 assert_eq!(
3409 parse_css_variable_brace_contents("--\u{1F600}"),
3410 Some(("\u{1F600}", None))
3411 );
3412 let r = catch(|| parse_css_variable_brace_contents("--"));
3414 assert!(r.is_ok(), "`--` panicked: {}", r.unwrap_err());
3415 }
3416
3417 #[test]
3418 fn check_if_value_is_css_var_recognises_var_syntax() {
3419 assert!(check_if_value_is_css_var("100px").is_none());
3421 assert!(check_if_value_is_css_var("").is_none());
3422 assert!(check_if_value_is_css_var("calc(1px + 2px)").is_none());
3423
3424 match check_if_value_is_css_var("var(--main-bg-color)") {
3426 Some(Ok((id, default))) => {
3427 assert_eq!(id, "main-bg-color");
3428 assert_eq!(default, "none");
3429 }
3430 other => panic!("expected Some(Ok(..)), got {other:?}"),
3431 }
3432
3433 match check_if_value_is_css_var("var(--w, 100px)") {
3435 Some(Ok((id, default))) => {
3436 assert_eq!(id, "w");
3437 assert_eq!(default.trim(), "100px");
3438 }
3439 other => panic!("expected Some(Ok(..)), got {other:?}"),
3440 }
3441
3442 assert!(matches!(
3444 check_if_value_is_css_var("var(nonsense)"),
3445 Some(Err(CssParseErrorInner::DynamicCssParseError(
3446 DynamicCssParseError::InvalidBraceContents(_)
3447 )))
3448 ));
3449 assert!(matches!(
3450 check_if_value_is_css_var("var()"),
3451 Some(Err(CssParseErrorInner::DynamicCssParseError(
3452 DynamicCssParseError::InvalidBraceContents(_)
3453 )))
3454 ));
3455 }
3456
3457 #[test]
3458 fn check_if_value_is_css_var_hostile_input_never_panics() {
3459 let long = format!("var(--{})", "x".repeat(100_000));
3460 let nested = format!("var({})", "(".repeat(10_000));
3461 let mut inputs: Vec<&str> = HOSTILE.to_vec();
3462 inputs.push(&long);
3463 inputs.push(&nested);
3464 inputs.push("var(");
3465 inputs.push("var)");
3466 inputs.push("var((--x))");
3467
3468 for input in inputs {
3469 let r = catch(|| check_if_value_is_css_var(input).is_some());
3470 assert!(
3471 r.is_ok(),
3472 "check_if_value_is_css_var({:.40?}) panicked: {}",
3473 input,
3474 r.unwrap_err()
3475 );
3476 }
3477 }
3478
3479 #[test]
3482 fn parse_css_declaration_valid_minimal() {
3483 let km = key_map();
3484 let mut warnings = Vec::new();
3485 let mut declarations = Vec::new();
3486 let r = parse_css_declaration(
3487 "width",
3488 "100px",
3489 loc(0, 0),
3490 &km,
3491 &mut warnings,
3492 &mut declarations,
3493 );
3494 assert_eq!(r, Ok(()));
3495 assert_eq!(declarations.len(), 1);
3496 assert!(matches!(declarations[0], CssDeclaration::Static(_)));
3497 assert!(warnings.is_empty());
3498 }
3499
3500 #[test]
3501 fn parse_css_declaration_unknown_key_is_downgraded_to_a_warning() {
3502 let km = key_map();
3503 let mut warnings = Vec::new();
3504 let mut declarations = Vec::new();
3505 let r = parse_css_declaration(
3508 "definitely-not-a-property",
3509 "1",
3510 loc(0, 0),
3511 &km,
3512 &mut warnings,
3513 &mut declarations,
3514 );
3515 assert_eq!(r, Ok(()));
3516 assert!(declarations.is_empty());
3517 assert_eq!(warnings.len(), 1);
3518 assert!(matches!(
3519 warnings[0].warning,
3520 CssParseWarnMsgInner::UnsupportedKeyValuePair { .. }
3521 ));
3522 }
3523
3524 #[test]
3525 fn parse_css_declaration_bad_value_is_a_hard_error() {
3526 let km = key_map();
3527 let mut warnings = Vec::new();
3528 let mut declarations = Vec::new();
3529 let r = parse_css_declaration(
3530 "width",
3531 "definitely-not-a-length",
3532 loc(0, 0),
3533 &km,
3534 &mut warnings,
3535 &mut declarations,
3536 );
3537 assert!(
3538 r.is_err(),
3539 "a known key with an unparseable value must error"
3540 );
3541 assert!(declarations.is_empty());
3542 }
3543
3544 #[test]
3545 fn parse_declaration_resilient_var_on_shorthand_is_rejected() {
3546 let km = key_map();
3547 let r = parse_declaration_resilient("margin", "var(--m)", loc(0, 0), &km);
3549 assert!(
3550 matches!(r, Err(CssParseErrorInner::VarOnShorthandProperty { .. })),
3551 "expected VarOnShorthandProperty, got {r:?}"
3552 );
3553 }
3554
3555 #[test]
3556 fn parse_declaration_resilient_var_on_normal_property_becomes_dynamic() {
3557 let km = key_map();
3558 let decls = parse_declaration_resilient("width", "var(--w, 100px)", loc(0, 0), &km)
3559 .expect("var() on a non-shorthand property should parse");
3560 assert_eq!(decls.len(), 1);
3561 match &decls[0] {
3562 CssDeclaration::Dynamic(DynamicCssProperty { dynamic_id, .. }) => {
3563 assert_eq!(dynamic_id.as_str(), "w");
3564 }
3565 other => panic!("expected a Dynamic declaration, got {other:?}"),
3566 }
3567 }
3568
3569 #[test]
3570 fn parse_declaration_resilient_hostile_key_value_pairs_never_panic() {
3571 let km = key_map();
3572 let long = "x".repeat(100_000);
3573 let mut inputs: Vec<&str> = HOSTILE.to_vec();
3574 inputs.push(&long);
3575
3576 for key in &inputs {
3577 for value in &inputs {
3578 let r = catch(|| parse_declaration_resilient(key, value, loc(0, 0), &km).is_ok());
3579 assert!(
3580 r.is_ok(),
3581 "parse_declaration_resilient({:.30?}, {:.30?}) panicked: {}",
3582 key,
3583 value,
3584 r.unwrap_err()
3585 );
3586 }
3587 }
3588 }
3589
3590 #[test]
3591 fn parse_declaration_resilient_empty_key_is_an_unknown_property() {
3592 let km = key_map();
3593 assert!(matches!(
3594 parse_declaration_resilient("", "", loc(0, 0), &km),
3595 Err(CssParseErrorInner::UnknownPropertyKey("", ""))
3596 ));
3597 }
3598
3599 #[test]
3604 fn get_line_column_from_error_representative_values() {
3605 let css = "div {\n width: 100px;\n}";
3606 assert_eq!(
3608 ErrorLocation { original_pos: 0 }.get_line_column_from_error(css),
3609 (0, 0)
3610 );
3611 let (line, _col) = ErrorLocation { original_pos: 12 }.get_line_column_from_error(css);
3612 assert_eq!(line, 2, "byte 11 is on the second line");
3613 }
3614
3615 #[test]
3616 fn get_line_column_from_error_empty_css_does_not_panic() {
3617 let r = catch(|| ErrorLocation { original_pos: 0 }.get_line_column_from_error(""));
3618 assert_eq!(r, Ok((0, 0)));
3619 }
3620
3621 #[test]
3624 fn get_line_column_from_error_newline_heavy_input_does_not_underflow() {
3625 let newlines = "\n".repeat(1_000);
3626 let crlf = "\r\n".repeat(1_000);
3627 for css in [newlines.as_str(), crlf.as_str()] {
3628 for pos in [1_usize, 2, 3, 500, css.len()] {
3629 let r =
3630 catch(|| ErrorLocation { original_pos: pos }.get_line_column_from_error(css));
3631 assert!(
3632 r.is_ok(),
3633 "get_line_column_from_error(pos={pos}) underflowed/panicked: {}",
3634 r.unwrap_err()
3635 );
3636 }
3637 }
3638 }
3639
3640 #[test]
3646 fn get_line_column_from_error_out_of_range_pos_does_not_panic() {
3647 let css = "div {}";
3648 for pos in [css.len() + 2, 999, usize::MAX] {
3649 if let Err(msg) =
3650 catch(|| ErrorLocation { original_pos: pos }.get_line_column_from_error(css))
3651 {
3652 panic!(
3653 "BUG: get_line_column_from_error panicked for original_pos={pos} on a \
3654 {}-byte string (unchecked `css_string[0..error_location]` slice); it \
3655 should clamp instead: {msg}",
3656 css.len()
3657 );
3658 }
3659 }
3660 }
3661
3662 #[test]
3668 fn get_line_column_from_error_at_end_of_unicode_css_does_not_panic() {
3669 let css = "a\u{1F600}";
3671 assert_eq!(css.len(), 5);
3672 let pos = css.len(); if let Err(msg) =
3674 catch(|| ErrorLocation { original_pos: pos }.get_line_column_from_error(css))
3675 {
3676 panic!(
3677 "BUG: get_line_column_from_error panicked at end-of-stream (original_pos={pos}) \
3678 because the CSS ends with a multi-byte char and `original_pos - 1` splits it: \
3679 {msg}"
3680 );
3681 }
3682 }
3683
3684 #[test]
3689 fn get_error_string_returns_the_trimmed_slice_between_start_and_end() {
3690 let css = "div { width: 100px; }";
3691 let err = CssParseError {
3692 css_string: css,
3693 error: CssParseErrorInner::MalformedCss,
3694 location: loc(6, 18),
3695 };
3696 assert_eq!(err.get_error_string(), "width: 100px");
3697
3698 let err = CssParseError {
3700 css_string: css,
3701 error: CssParseErrorInner::MalformedCss,
3702 location: loc(0, 0),
3703 };
3704 assert_eq!(err.get_error_string(), "");
3705 }
3706
3707 #[test]
3713 fn get_error_string_invalid_location_does_not_panic() {
3714 let cases: [(&str, ErrorLocationRange, &str); 3] = [
3715 ("div", loc(0, 99), "end past the end of the string"),
3716 ("div", loc(2, 1), "reversed range (start > end)"),
3717 ("a\u{1F600}", loc(0, 4), "end inside a multi-byte char"),
3718 ];
3719 for (css, location, why) in cases {
3720 let err = CssParseError {
3721 css_string: css,
3722 error: CssParseErrorInner::MalformedCss,
3723 location,
3724 };
3725 if let Err(msg) = catch(|| err.get_error_string().to_string()) {
3726 panic!(
3727 "BUG: get_error_string panicked on an invalid location ({why}) instead of \
3728 returning an empty/clamped slice: {msg}"
3729 );
3730 }
3731 }
3732 }
3733
3734 #[test]
3737 fn display_of_css_parse_error_is_non_empty_and_well_formed() {
3738 let css = "div { width: 100px; }";
3739 let err = CssParseError {
3740 css_string: css,
3741 error: CssParseErrorInner::MalformedCss,
3742 location: loc(6, 18),
3743 };
3744 let s = format!("{err}");
3745 assert!(!s.is_empty());
3746 assert!(s.contains("start: line"), "missing start location: {s}");
3747 assert!(s.contains("end: line"), "missing end location: {s}");
3748 assert!(s.contains("width: 100px"), "missing offending text: {s}");
3749 assert!(s.contains("Malformed Css"), "missing reason: {s}");
3750 }
3751
3752 #[test]
3753 fn display_of_css_parse_error_on_zero_value_does_not_panic() {
3754 let err = CssParseError {
3755 css_string: "",
3756 error: CssParseErrorInner::UnclosedBlock,
3757 location: ErrorLocationRange::default(),
3758 };
3759 let r = catch(|| format!("{err}"));
3760 match r {
3761 Ok(s) => assert!(!s.is_empty(), "Display produced an empty string"),
3762 Err(msg) => panic!("Display panicked on a zero-valued CssParseError: {msg}"),
3763 }
3764 }
3765
3766 #[test]
3771 fn display_of_css_parse_error_with_unicode_css_does_not_panic() {
3772 let css = "p{}\u{1F600}"; let err = CssParseError {
3774 css_string: css,
3775 error: CssParseErrorInner::MalformedCss,
3776 location: loc(0, css.len()),
3777 };
3778 if let Err(msg) = catch(|| format!("{err}")) {
3779 panic!(
3780 "BUG: Display for CssParseError panicked while formatting an error whose CSS \
3781 ends in a multi-byte char (unchecked slicing in get_line_column_from_error / \
3782 get_error_string): {msg}"
3783 );
3784 }
3785 }
3786
3787 #[test]
3788 fn display_of_error_and_warning_inners_is_never_empty() {
3789 let km = key_map();
3790 let margin =
3791 CombinedCssPropertyType::from_str("margin", &km).expect("margin is a shorthand");
3792
3793 let inners: Vec<CssParseErrorInner<'_>> = vec![
3794 CssParseErrorInner::ParseError(CssSyntaxError::UnknownToken(CssSyntaxErrorPos {
3795 row: usize::MAX,
3796 col: usize::MAX,
3797 })),
3798 CssParseErrorInner::UnclosedBlock,
3799 CssParseErrorInner::MalformedCss,
3800 CssParseErrorInner::DynamicCssParseError(DynamicCssParseError::InvalidBraceContents(
3801 "",
3802 )),
3803 CssParseErrorInner::PseudoSelectorParseError(
3804 CssPseudoSelectorParseError::EmptyNthChild,
3805 ),
3806 CssParseErrorInner::NodeTypeTag(NodeTypeTagParseError::Invalid("")),
3807 CssParseErrorInner::UnknownPropertyKey("", ""),
3808 CssParseErrorInner::VarOnShorthandProperty {
3809 key: margin,
3810 value: "",
3811 },
3812 ];
3813
3814 for inner in &inners {
3815 let s = format!("{inner}");
3816 assert!(!s.is_empty(), "empty Display for {inner:?}");
3817 }
3818
3819 let warnings = vec![
3820 CssParseWarnMsgInner::UnsupportedKeyValuePair { key: "", value: "" },
3821 CssParseWarnMsgInner::ParseError(CssParseErrorInner::MalformedCss),
3822 CssParseWarnMsgInner::SkippedRule {
3823 selector: None,
3824 error: CssParseErrorInner::UnclosedBlock,
3825 },
3826 CssParseWarnMsgInner::SkippedDeclaration {
3827 key: "",
3828 value: "",
3829 error: CssParseErrorInner::MalformedCss,
3830 },
3831 CssParseWarnMsgInner::MalformedStructure { message: "" },
3832 ];
3833 for w in &warnings {
3834 assert!(!format!("{w}").is_empty(), "empty Display for {w:?}");
3835 }
3836 }
3837
3838 #[test]
3843 fn css_pseudo_selector_parse_error_round_trips() {
3844 let cases = vec![
3845 CssPseudoSelectorParseError::EmptyNthChild,
3846 CssPseudoSelectorParseError::UnknownSelector("blah", None),
3847 CssPseudoSelectorParseError::UnknownSelector("blah", Some("3")),
3848 CssPseudoSelectorParseError::InvalidNthChildPattern("2x+1"),
3849 CssPseudoSelectorParseError::InvalidNthChild("x".parse::<u32>().unwrap_err()),
3850 CssPseudoSelectorParseError::InvalidNthChild(
3851 "99999999999999999999".parse::<u32>().unwrap_err(),
3852 ),
3853 CssPseudoSelectorParseError::UnknownSelector("", Some("")),
3855 ];
3856 for case in &cases {
3857 assert_eq!(
3858 &case.to_contained().to_shared(),
3859 case,
3860 "round-trip changed the value"
3861 );
3862 }
3863 }
3864
3865 #[test]
3866 fn dynamic_css_parse_error_round_trips() {
3867 let simple = DynamicCssParseError::InvalidBraceContents("--x, blue");
3868 assert_eq!(&simple.to_contained().to_shared(), &simple);
3869
3870 let empty = DynamicCssParseError::InvalidBraceContents("");
3871 assert_eq!(&empty.to_contained().to_shared(), &empty);
3872
3873 let km = key_map();
3876 let width = CssPropertyType::from_str("width", &km).expect("width is a property");
3877 let inner = parse_css_property(width, "definitely-not-a-length")
3878 .expect_err("an invalid length must fail to parse");
3879 let wrapped = DynamicCssParseError::UnexpectedValue(inner);
3880 let round_tripped = wrapped.to_contained();
3881 let back = round_tripped.to_shared();
3882 assert!(matches!(back, DynamicCssParseError::UnexpectedValue(_)));
3883 assert_eq!(
3884 format!("{back}"),
3885 format!("{wrapped}"),
3886 "round-trip lost information from the nested CssParsingError"
3887 );
3888 }
3889
3890 #[test]
3891 fn css_parse_error_inner_round_trips_for_every_variant() {
3892 let km = key_map();
3893 let margin =
3894 CombinedCssPropertyType::from_str("margin", &km).expect("margin is a shorthand");
3895
3896 let cases = vec![
3897 CssParseErrorInner::ParseError(CssSyntaxError::UnexpectedEndOfStream(
3898 CssSyntaxErrorPos { row: 0, col: 0 },
3899 )),
3900 CssParseErrorInner::ParseError(CssSyntaxError::InvalidAdvance(
3902 CssSyntaxInvalidAdvance {
3903 expected: isize::MIN,
3904 total: usize::MAX,
3905 pos: CssSyntaxErrorPos {
3906 row: usize::MAX,
3907 col: usize::MAX,
3908 },
3909 },
3910 )),
3911 CssParseErrorInner::ParseError(CssSyntaxError::UnsupportedToken(CssSyntaxErrorPos {
3912 row: 3,
3913 col: 7,
3914 })),
3915 CssParseErrorInner::UnclosedBlock,
3916 CssParseErrorInner::MalformedCss,
3917 CssParseErrorInner::DynamicCssParseError(DynamicCssParseError::InvalidBraceContents(
3918 "--x",
3919 )),
3920 CssParseErrorInner::PseudoSelectorParseError(
3921 CssPseudoSelectorParseError::EmptyNthChild,
3922 ),
3923 CssParseErrorInner::NodeTypeTag(NodeTypeTagParseError::Invalid("notatag")),
3924 CssParseErrorInner::UnknownPropertyKey("key", "value"),
3925 CssParseErrorInner::UnknownPropertyKey("", ""),
3926 CssParseErrorInner::VarOnShorthandProperty {
3927 key: margin,
3928 value: "var(--m)",
3929 },
3930 ];
3931
3932 for case in &cases {
3933 assert_eq!(
3934 &case.to_contained().to_shared(),
3935 case,
3936 "round-trip changed the value for {case:?}"
3937 );
3938 }
3939 }
3940
3941 #[test]
3942 fn css_parse_error_round_trips_including_unicode_payloads() {
3943 let css = "div { width: \u{1F600}; }";
3944 let err = CssParseError {
3945 css_string: css,
3946 error: CssParseErrorInner::UnknownPropertyKey("k\u{1F600}", "v\u{4E2D}"),
3947 location: loc(1, 2),
3948 };
3949 assert_eq!(err.to_contained().to_shared(), err);
3950
3951 let err = CssParseError {
3953 css_string: "",
3954 error: CssParseErrorInner::MalformedCss,
3955 location: ErrorLocationRange::default(),
3956 };
3957 assert_eq!(err.to_contained().to_shared(), err);
3958 }
3959
3960 #[test]
3961 fn css_path_parse_error_round_trips_for_every_variant() {
3962 let cases = vec![
3963 CssPathParseError::EmptyPath,
3964 CssPathParseError::InvalidTokenEncountered("{"),
3965 CssPathParseError::InvalidTokenEncountered(""),
3966 CssPathParseError::UnexpectedEndOfStream("div"),
3967 CssPathParseError::SyntaxError(CssSyntaxError::UnknownToken(CssSyntaxErrorPos {
3968 row: usize::MAX,
3969 col: 0,
3970 })),
3971 CssPathParseError::NodeTypeTag(NodeTypeTagParseError::Invalid("notatag")),
3972 CssPathParseError::PseudoSelectorParseError(
3973 CssPseudoSelectorParseError::InvalidNthChildPattern("2x"),
3974 ),
3975 ];
3976 for case in &cases {
3977 assert_eq!(
3978 &case.to_contained().to_shared(),
3979 case,
3980 "round-trip changed the value for {case:?}"
3981 );
3982 }
3983 }
3984
3985 #[test]
3986 fn css_parse_warn_msg_round_trips_for_every_variant() {
3987 let inners = vec![
3988 CssParseWarnMsgInner::UnsupportedKeyValuePair {
3989 key: "foo",
3990 value: "bar",
3991 },
3992 CssParseWarnMsgInner::UnsupportedKeyValuePair { key: "", value: "" },
3993 CssParseWarnMsgInner::ParseError(CssParseErrorInner::MalformedCss),
3994 CssParseWarnMsgInner::SkippedRule {
3995 selector: None,
3996 error: CssParseErrorInner::UnclosedBlock,
3997 },
3998 CssParseWarnMsgInner::SkippedRule {
3999 selector: Some("div"),
4000 error: CssParseErrorInner::MalformedCss,
4001 },
4002 CssParseWarnMsgInner::SkippedDeclaration {
4003 key: "width",
4004 value: "\u{1F600}",
4005 error: CssParseErrorInner::MalformedCss,
4006 },
4007 CssParseWarnMsgInner::MalformedStructure {
4008 message: "unclosed",
4009 },
4010 ];
4011
4012 for inner in &inners {
4013 assert_eq!(
4014 &inner.to_contained().to_shared(),
4015 inner,
4016 "round-trip changed the value for {inner:?}"
4017 );
4018
4019 let msg = CssParseWarnMsg {
4020 warning: inner.clone(),
4021 location: loc(7, 42),
4022 };
4023 let back = msg.to_contained();
4024 let back = back.to_shared();
4025 assert_eq!(back, msg, "CssParseWarnMsg round-trip changed the value");
4026 assert_eq!(back.location, loc(7, 42), "location was not preserved");
4027 }
4028 }
4029
4030 #[test]
4031 fn unparsed_css_rule_block_round_trips() {
4032 let mut declarations = BTreeMap::new();
4033 declarations.insert("width", ("100px", loc(1, 2)));
4034 declarations.insert("color", ("\u{1F600}", loc(3, 4)));
4035
4036 let block = UnparsedCssRuleBlock {
4037 path: CssPath {
4038 selectors: vec![
4039 CssPathSelector::Global,
4040 CssPathSelector::Class("btn".to_string().into()),
4041 ]
4042 .into(),
4043 },
4044 declarations,
4045 conditions: vec![DynamicSelector::Media(MediaType::Screen)],
4049 };
4050
4051 assert_eq!(block.to_contained().to_shared(), block);
4052
4053 let empty = UnparsedCssRuleBlock {
4055 path: CssPath {
4056 selectors: Vec::new().into(),
4057 },
4058 declarations: BTreeMap::new(),
4059 conditions: Vec::new(),
4060 };
4061 assert_eq!(empty.to_contained().to_shared(), empty);
4062 }
4063
4064 #[test]
4069 fn new_from_str_valid_minimal() {
4070 let (css, warnings) = new_from_str("div { width: 100px; }");
4071 assert_eq!(css.rules.len(), 1);
4072 assert_eq!(css.rules.as_slice()[0].declarations.len(), 1);
4073 assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
4074 }
4075
4076 #[test]
4077 fn new_from_str_empty_input_yields_an_empty_stylesheet() {
4078 let (css, warnings) = new_from_str("");
4079 assert_eq!(css.rules.len(), 0);
4080 assert!(warnings.is_empty());
4081
4082 let (css, _warnings) = new_from_str(" \n\t ");
4083 assert_eq!(css.rules.len(), 0);
4084 }
4085
4086 #[test]
4087 fn new_from_str_unclosed_block_warns_instead_of_failing() {
4088 let (css, warnings) = new_from_str("div { width: 100px;");
4089 assert_eq!(css.rules.len(), 0, "an unclosed block emits no rules");
4090 assert!(
4091 warnings
4092 .iter()
4093 .any(|w| matches!(w.warning, CssParseWarnMsgInner::MalformedStructure { .. })),
4094 "expected a MalformedStructure warning, got {warnings:?}"
4095 );
4096 }
4097
4098 #[test]
4099 fn new_from_str_unknown_property_is_a_warning_not_a_dropped_rule() {
4100 let (css, warnings) = new_from_str("div { definitely-not-a-property: 1; width: 10px; }");
4101 assert_eq!(css.rules.len(), 1);
4102 assert_eq!(css.rules.as_slice()[0].declarations.len(), 1);
4104 assert!(!warnings.is_empty(), "the unknown key should have warned");
4105 }
4106
4107 #[test]
4108 fn var_reference_resolves_against_a_root_custom_property() {
4109 let (css, _) = new_from_str(":root{--boxw:150px} .v{width:var(--boxw)}");
4110 let decls: Vec<_> = css
4113 .rules
4114 .as_slice()
4115 .iter()
4116 .flat_map(|r| r.declarations.as_slice().iter())
4117 .collect();
4118 assert_eq!(
4119 decls.len(),
4120 1,
4121 "custom-prop def emits nothing, var() resolves: {decls:?}"
4122 );
4123 let (direct, _) = new_from_str(".v{width:150px}");
4125 assert_eq!(
4126 decls[0],
4127 &direct.rules.as_slice()[0].declarations.as_slice()[0]
4128 );
4129 }
4130
4131 #[test]
4132 fn undefined_var_reference_falls_back_to_its_default() {
4133 let (css, _) = new_from_str(".v{width:var(--nope, 42px)}");
4134 let (direct, _) = new_from_str(".v{width:42px}");
4135 assert_eq!(
4136 css.rules.as_slice()[0].declarations.as_slice()[0],
4137 direct.rules.as_slice()[0].declarations.as_slice()[0],
4138 );
4139 }
4140
4141 #[test]
4143 fn new_from_str_hostile_input_never_panics() {
4144 let long_rule = "div { width: 100px; }".repeat(2_000);
4145 let deep_nesting = format!("{}{}", "div {".repeat(500), "}".repeat(500));
4146 let unbalanced_open = "{".repeat(5_000);
4147 let unbalanced_close = "}".repeat(5_000);
4148 let long_selector = format!("{} {{ width: 1px; }}", "div ".repeat(10_000));
4149
4150 let mut inputs: Vec<&str> = HOSTILE.to_vec();
4151 inputs.push(&long_rule);
4152 inputs.push(&deep_nesting);
4153 inputs.push(&unbalanced_open);
4154 inputs.push(&unbalanced_close);
4155 inputs.push(&long_selector);
4156 inputs.push("div { width: \u{1F600}; }");
4157 inputs.push("\u{1F600} { \u{4E2D}: \u{6587}; }");
4158 inputs.push("div { width: 100px; /* unterminated");
4159 inputs.push("@media (min-width: 800px) { div { width: 1px; } }");
4160 inputs.push("@theme(dark) { div { width: 1px; } }");
4161 inputs.push("@lang(\"de-DE\") { div { width: 1px; } }");
4162 inputs.push("@container sidebar (min-width: 400px) { div { width: 1px; } }");
4163 inputs.push("@definitely-not-an-at-rule x { div { width: 1px; } }");
4164 inputs.push(".a { .b { :hover { width: 1px; } } }");
4165 inputs.push("div[data-x=\"y\"] { width: 1px; }");
4166 inputs.push("div[ { width: 1px; }");
4167 inputs.push("a, b, , c { width: 1px; }");
4168 inputs.push("div:nth-child(999999999999) { width: 1px; }");
4169
4170 for input in inputs {
4171 let r = catch(|| {
4172 let (css, warnings) = new_from_str(input);
4173 (css.rules.len(), warnings.len())
4174 });
4175 assert!(
4176 r.is_ok(),
4177 "new_from_str({:.60?}) panicked despite the `Never panics` contract: {}",
4178 input,
4179 r.unwrap_err()
4180 );
4181 }
4182 }
4183
4184 #[test]
4185 fn new_from_str_at_rules_attach_conditions_to_nested_rules() {
4186 let (css, _warnings) = new_from_str("@media screen { div { width: 1px; } }");
4187 assert_eq!(css.rules.len(), 1);
4188 let rule = &css.rules.as_slice()[0];
4189 assert!(
4190 rule.conditions
4191 .as_slice()
4192 .contains(&DynamicSelector::Media(MediaType::Screen)),
4193 "the @media condition was not attached: {:?}",
4194 rule.conditions.as_slice()
4195 );
4196 }
4197
4198 #[test]
4199 fn new_from_str_comma_separated_selectors_emit_one_rule_each() {
4200 let (css, _warnings) = new_from_str("div, p { width: 1px; }");
4201 assert_eq!(
4202 css.rules.len(),
4203 2,
4204 "each selector in the list gets its own rule"
4205 );
4206 }
4207
4208 #[test]
4209 fn new_from_str_inner_matches_new_from_str() {
4210 let css_string = "div { width: 100px; }";
4211 let mut tokenizer = Tokenizer::new(css_string);
4212 let mut kf = Vec::new();
4213 let (rules, warnings) = new_from_str_inner(css_string, &mut tokenizer, &mut kf);
4214 assert_eq!(rules.len(), 1);
4215 assert!(warnings.is_empty());
4216 }
4217
4218 #[test]
4219 fn get_error_location_tracks_the_tokenizer_position() {
4220 let css_string = "div { width: 100px; }";
4221 let mut tokenizer = Tokenizer::new(css_string);
4222 assert_eq!(get_error_location(&tokenizer).original_pos, 0);
4223
4224 let _ = tokenizer.parse_next();
4225 let after = get_error_location(&tokenizer).original_pos;
4226 assert!(after > 0, "the tokenizer position did not advance");
4227 assert!(
4228 after <= css_string.len(),
4229 "the tokenizer position ran past the end of the input"
4230 );
4231
4232 let empty = Tokenizer::new("");
4234 assert_eq!(get_error_location(&empty).original_pos, 0);
4235 }
4236
4237 #[test]
4238 fn css_blocks_to_stylesheet_parses_known_keys_and_warns_on_unknown_ones() {
4239 let css_string = "div { width: 100px; }";
4240
4241 let mut declarations = BTreeMap::new();
4242 declarations.insert("width", ("100px", loc(6, 18)));
4243 let good = UnparsedCssRuleBlock {
4244 path: CssPath {
4245 selectors: vec![CssPathSelector::Global].into(),
4246 },
4247 declarations,
4248 conditions: Vec::new(),
4249 };
4250
4251 let mut declarations = BTreeMap::new();
4252 declarations.insert("definitely-not-a-property", ("1", loc(0, 1)));
4253 let bad = UnparsedCssRuleBlock {
4254 path: CssPath {
4255 selectors: vec![CssPathSelector::Global].into(),
4256 },
4257 declarations,
4258 conditions: Vec::new(),
4259 };
4260
4261 let (rules, warnings) = css_blocks_to_stylesheet(vec![good, bad], css_string);
4262 assert_eq!(rules.len(), 2, "both blocks are emitted");
4263 assert_eq!(rules[0].declarations.len(), 1);
4264 assert_eq!(rules[1].declarations.len(), 0, "the unknown key is dropped");
4265 assert_eq!(
4266 warnings.len(),
4267 1,
4268 "the unknown key produced exactly one warning"
4269 );
4270 assert!(matches!(
4271 warnings[0].warning,
4272 CssParseWarnMsgInner::SkippedDeclaration { .. }
4273 ));
4274 }
4275
4276 #[test]
4277 fn css_blocks_to_stylesheet_empty_input_is_empty_output() {
4278 let (rules, warnings) = css_blocks_to_stylesheet(Vec::new(), "");
4279 assert!(rules.is_empty());
4280 assert!(warnings.is_empty());
4281 }
4282}
4283
4284#[cfg(test)]
4285mod keyframes_tests {
4286 use super::*;
4287
4288 #[test]
4294 fn keyframes_inside_media_parse_and_rules_survive() {
4295 let css = r#"
4296 p { color: red; }
4297 @media (min-width: 100px) {
4298 @keyframes nested { from { opacity: 0; } 50% { opacity: 0.5; } to { opacity: 1; } }
4299 div { color: blue; }
4300 }
4301 span { color: green; }
4302 "#;
4303 let (parsed, _warnings) = new_from_str(css);
4304 let kf: Vec<_> = parsed.keyframes.as_ref().iter().collect();
4305 assert_eq!(
4306 kf.len(),
4307 1,
4308 "nested @keyframes must parse: {:?}",
4309 parsed.keyframes
4310 );
4311 assert_eq!(kf[0].name.as_str(), "nested");
4312 let permilles: Vec<u16> = kf[0].stops.iter().map(|s| s.permille).collect();
4313 assert_eq!(permilles, vec![0, 500, 1000]);
4314 let total_rules: usize = parsed.rules.as_ref().len();
4316 assert_eq!(total_rules, 3, "p + div + span: {:#?}", parsed.rules);
4317 }
4318
4319 #[test]
4323 fn keyframes_inside_comment_do_not_register() {
4324 let css = r#"
4325 /* @keyframes ghost { from { opacity: 0; } to { opacity: 1; } } */
4326 p { color: red; }
4327 "#;
4328 let (parsed, _warnings) = new_from_str(css);
4329 assert_eq!(
4330 parsed.keyframes.as_ref().len(),
4331 0,
4332 "commented-out @keyframes registered: {:?}",
4333 parsed.keyframes
4334 );
4335 assert_eq!(parsed.rules.as_ref().len(), 1);
4336 }
4337
4338 #[test]
4342 fn keyframes_fractional_and_comma_list_stops() {
4343 let css = "@keyframes k { 62.5%, to { opacity: 1; } }";
4344 let (parsed, _warnings) = new_from_str(css);
4345 let kf: Vec<_> = parsed.keyframes.as_ref().iter().collect();
4346 assert_eq!(kf.len(), 1);
4347 let permilles: Vec<u16> = kf[0].stops.iter().map(|s| s.permille).collect();
4348 assert_eq!(permilles, vec![625, 1000]);
4349 assert_eq!(kf[0].stops.as_ref()[0].props.as_ref().len(), 1);
4350 assert_eq!(kf[0].stops.as_ref()[1].props.as_ref().len(), 1);
4351 }
4352
4353 #[test]
4357 fn keyframes_parse_and_do_not_disturb_rules() {
4358 let css = r#"
4359 div { width: 50px; }
4360 @keyframes flyOutRight {
4361 from { transform: translateX(0px); opacity: 1; }
4362 50% { opacity: 0.75; }
4363 to { transform: translateX(200px); width: 0px; }
4364 }
4365 p { height: 10px; }
4366 "#;
4367 let (parsed, warnings) = new_from_str(css);
4368 assert!(
4369 warnings.is_empty(),
4370 "keyframes must not produce rule-parser warnings: {warnings:#?}"
4371 );
4372 assert_eq!(parsed.keyframes.as_ref().len(), 1);
4373 let kf = &parsed.keyframes.as_ref()[0];
4374 assert_eq!(kf.name.as_str(), "flyOutRight");
4375 let stops = kf.stops.as_ref();
4376 assert_eq!(stops.len(), 3);
4377 assert_eq!(stops[0].permille, 0);
4378 assert_eq!(stops[1].permille, 500);
4379 assert_eq!(stops[2].permille, 1000);
4380 assert_eq!(
4381 stops[0].props.as_ref().len(),
4382 2,
4383 "from: transform + opacity"
4384 );
4385 assert_eq!(stops[2].props.as_ref().len(), 2, "to: transform + width");
4386
4387 let (no_kf, _) = new_from_str("div { width: 50px; } p { height: 10px; }");
4390 assert_eq!(parsed.rules.as_ref().len(), no_kf.rules.as_ref().len());
4391 }
4392
4393 #[test]
4396 fn animation_properties_parse() {
4397 let css = r#"
4398 #sidebar {
4399 -azul-animation-out: flyOutRight 1s;
4400 -azul-animation-in: flyInLeft 500ms spring;
4401 animation: all 2s ease-out;
4402 }
4403 "#;
4404 let (parsed, warnings) = new_from_str(css);
4405 assert!(warnings.is_empty(), "{warnings:#?}");
4406 let rules = parsed.rules.as_ref();
4407 assert_eq!(rules.len(), 1);
4408 let decls = rules[0].declarations.as_ref();
4409 assert_eq!(decls.len(), 3, "{decls:#?}");
4410 let mut found_out = false;
4411 let mut found_all = false;
4412 for d in decls {
4413 let crate::css::CssDeclaration::Static(prop) = d else {
4414 continue;
4415 };
4416 if let crate::props::property::CssProperty::AnimationOut(v) = prop {
4417 let list = v.get_property().cloned().unwrap_or_default();
4418 let a = &list.as_ref()[0];
4419 assert_eq!(a.name.as_str(), "flyOutRight");
4420 assert_eq!(
4421 a.duration,
4422 crate::props::basic::time::CssDuration::from_millis(1000)
4423 );
4424 found_out = true;
4425 }
4426 if let crate::props::property::CssProperty::Animation(v) = prop {
4427 let list = v.get_property().cloned().unwrap_or_default();
4428 let a = &list.as_ref()[0];
4429 assert_eq!(a.name.as_str(), "all");
4430 assert_eq!(
4431 a.duration,
4432 crate::props::basic::time::CssDuration::from_millis(2000)
4433 );
4434 assert_eq!(
4435 a.timing,
4436 crate::props::basic::animation::AnimationTiming::EaseOut
4437 );
4438 found_all = true;
4439 }
4440 }
4441 assert!(found_out && found_all);
4442 }
4443}
4444
4445#[cfg(test)]
4452mod env_tests {
4453 use super::*;
4454 use crate::{
4455 css::CssDeclaration,
4456 dynamic_selector::{DynamicSelectorContext, EnvVariable},
4457 props::property::{parse_css_property, CssPropertyType},
4458 };
4459
4460 fn only_declaration(css: &str) -> CssDeclaration {
4461 let (parsed, warnings) = new_from_str(css);
4462 assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
4463 let rules: Vec<_> = parsed.rules().collect();
4464 assert_eq!(rules.len(), 1, "one rule expected");
4465 let decls = rules[0].declarations.as_ref();
4466 assert_eq!(decls.len(), 1, "one declaration expected, got {decls:?}");
4467 decls[0].clone()
4468 }
4469
4470 #[test]
4471 fn env_with_fallback_parses_to_a_dynamic_env_declaration() {
4472 let d = only_declaration("div { padding-bottom: env(safe-area-inset-bottom, 7px); }");
4473 assert_eq!(d.env_variable(), Some(EnvVariable::SafeAreaInsetBottom));
4474 assert_eq!(d.get_type(), CssPropertyType::PaddingBottom);
4475 assert!(d.is_cascade_resolvable());
4476 assert!(d.depends_on_dynamic_context());
4477 let seven = parse_css_property(CssPropertyType::PaddingBottom, "7px").unwrap();
4479 assert_eq!(d.resolve_in_cascade(None), Some(seven));
4480 }
4481
4482 #[test]
4483 fn env_without_fallback_defaults_to_zero() {
4484 let d = only_declaration("div { margin-top: env(safe-area-inset-top); }");
4485 assert_eq!(d.env_variable(), Some(EnvVariable::SafeAreaInsetTop));
4486 let zero = parse_css_property(CssPropertyType::MarginTop, "0px").unwrap();
4487 assert_eq!(d.resolve_in_cascade(None), Some(zero));
4488 }
4489
4490 #[test]
4491 fn every_defined_name_round_trips_through_the_parser() {
4492 for v in EnvVariable::ALL {
4493 let css = format!("div {{ top: env({}, 1px); }}", v.as_css_name());
4494 let d = only_declaration(&css);
4495 assert_eq!(d.env_variable(), Some(v), "{css}");
4496 assert_eq!(
4497 EnvVariable::from_dynamic_id(v.dynamic_id().as_str()),
4498 Some(v)
4499 );
4500 }
4501 assert_eq!(EnvVariable::from_dynamic_id("my-var"), None);
4502 }
4503
4504 #[test]
4505 fn unknown_env_name_with_a_fallback_is_the_fallback_statically() {
4506 let d = only_declaration("div { padding-bottom: env(no-such-thing, 7px); }");
4507 assert_eq!(d.env_variable(), None);
4508 let seven = parse_css_property(CssPropertyType::PaddingBottom, "7px").unwrap();
4509 assert_eq!(d, CssDeclaration::Static(seven));
4510 }
4511
4512 #[test]
4513 fn unknown_env_name_without_a_fallback_is_dropped_with_a_warning() {
4514 let (parsed, warnings) = new_from_str("div { padding-bottom: env(no-such-thing); }");
4515 assert_eq!(warnings.len(), 1, "{warnings:?}");
4516 let rules: Vec<_> = parsed.rules().collect();
4517 assert!(
4518 rules.iter().all(|r| r.declarations.as_ref().is_empty()),
4519 "the declaration must not survive: {rules:?}"
4520 );
4521 }
4522
4523 #[test]
4524 fn env_on_a_shorthand_expands_to_the_longhands() {
4525 let (parsed, warnings) = new_from_str("div { padding: env(safe-area-inset-top, 4px); }");
4526 assert!(warnings.is_empty(), "{warnings:?}");
4527 let rules: Vec<_> = parsed.rules().collect();
4528 let decls = rules[0].declarations.as_ref();
4529 assert_eq!(decls.len(), 4, "{decls:?}");
4530 for d in decls {
4531 assert_eq!(d.env_variable(), Some(EnvVariable::SafeAreaInsetTop));
4532 }
4533 let types: Vec<_> = decls.iter().map(CssDeclaration::get_type).collect();
4534 assert!(types.contains(&CssPropertyType::PaddingLeft));
4535 assert!(types.contains(&CssPropertyType::PaddingBottom));
4536 }
4537
4538 #[test]
4539 fn env_is_not_mistaken_for_var_and_survives_var_substitution() {
4540 let (parsed, warnings) = new_from_str(
4543 ":root { --gap: 3px; } div { margin-left: var(--gap); \
4544 padding-bottom: env(safe-area-inset-bottom, 7px); }",
4545 );
4546 assert!(warnings.is_empty(), "{warnings:?}");
4547 let div = parsed
4548 .rules()
4549 .find(|r| r.declarations.as_ref().len() == 2)
4550 .expect("the div rule");
4551 let decls = div.declarations.as_ref();
4552 let three = parse_css_property(CssPropertyType::MarginLeft, "3px").unwrap();
4553 assert_eq!(decls[0], CssDeclaration::Static(three));
4554 assert_eq!(
4555 decls[1].env_variable(),
4556 Some(EnvVariable::SafeAreaInsetBottom)
4557 );
4558 assert!(div.depends_on_dynamic_context());
4559 }
4560
4561 #[test]
4562 fn resolve_in_cascade_reads_the_live_inset_and_keeps_the_declared_type() {
4563 let d = only_declaration("div { padding-bottom: env(safe-area-inset-bottom, 7px); }");
4564 let mut ctx = DynamicSelectorContext::default();
4565 ctx.safe_area_bottom = 34.0;
4566 let live = parse_css_property(CssPropertyType::PaddingBottom, "34px").unwrap();
4567 assert_eq!(d.resolve_in_cascade(Some(&ctx)), Some(live));
4568
4569 ctx.safe_area_bottom = f32::NAN;
4571 let seven = parse_css_property(CssPropertyType::PaddingBottom, "7px").unwrap();
4572 assert_eq!(d.resolve_in_cascade(Some(&ctx)), Some(seven));
4573
4574 let w = only_declaration("div { width: env(safe-area-inset-left, 1em); }");
4577 ctx.safe_area_left = 20.5;
4578 let live_w = parse_css_property(CssPropertyType::Width, "20.5px").unwrap();
4579 assert_eq!(w.resolve_in_cascade(Some(&ctx)), Some(live_w));
4580 }
4581
4582 #[test]
4583 fn plain_values_and_calc_wrapped_env_are_untouched_by_the_env_check() {
4584 assert!(check_if_value_is_css_env("100px").is_none());
4585 assert!(check_if_value_is_css_env("var(--x, 1px)").is_none());
4586 assert!(check_if_value_is_css_env("calc(20px + env(safe-area-inset-bottom))").is_none());
4589 }
4590}