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 { row: p.row, col: p.col }
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[repr(C)]
36pub struct CssSyntaxInvalidAdvance {
37 pub expected: isize,
38 pub total: usize,
39 pub pos: CssSyntaxErrorPos,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44#[repr(C, u8)]
45pub enum CssSyntaxError {
46 UnexpectedEndOfStream(CssSyntaxErrorPos),
47 InvalidAdvance(CssSyntaxInvalidAdvance),
48 UnsupportedToken(CssSyntaxErrorPos),
49 UnknownToken(CssSyntaxErrorPos),
50}
51
52impl From<SimplecssError> for CssSyntaxError {
53 fn from(e: SimplecssError) -> Self {
54 match e {
55 SimplecssError::UnexpectedEndOfStream(pos) => Self::UnexpectedEndOfStream(pos.into()),
56 SimplecssError::InvalidAdvance { expected, total, pos } => Self::InvalidAdvance(CssSyntaxInvalidAdvance { expected, total, pos: pos.into() }),
57 SimplecssError::UnsupportedToken(pos) => Self::UnsupportedToken(pos.into()),
58 SimplecssError::UnknownToken(pos) => Self::UnknownToken(pos.into()),
59 }
60 }
61}
62
63pub use crate::props::property::CssParsingError;
64use crate::{
65 corety::{AzString, OptionString},
66 css::{
67 AttributeMatchOp, Css, CssAttributeSelector, CssDeclaration, CssNthChildSelector, CssPath,
68 CssPathPseudoSelector, CssPathSelector, CssRuleBlock, DynamicCssProperty, NodeTypeTag,
69 NodeTypeTagParseError, NodeTypeTagParseErrorOwned,
70 },
71 dynamic_selector::{
72 BoolCondition, DynamicSelector, DynamicSelectorVec, LanguageCondition, MediaType,
73 MinMaxRange, OrientationType, OsCondition, ThemeCondition, parse_os_version,
74 },
75 props::{
76 basic::parse::parse_parentheses,
77 property::{
78 parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
79 CssParsingErrorOwned, CssPropertyType,
80 },
81 },
82};
83
84#[derive(Debug, Clone, PartialEq)]
86pub struct CssParseError<'a> {
87 pub css_string: &'a str,
88 pub error: CssParseErrorInner<'a>,
89 pub location: ErrorLocationRange,
90}
91
92#[derive(Debug, Clone, PartialEq)]
94#[repr(C)]
95pub struct CssParseErrorOwned {
96 pub css_string: AzString,
97 pub error: CssParseErrorInnerOwned,
98 pub location: ErrorLocationRange,
99}
100
101impl CssParseError<'_> {
102 #[must_use] pub fn to_contained(&self) -> CssParseErrorOwned {
103 CssParseErrorOwned {
104 css_string: self.css_string.to_string().into(),
105 error: self.error.to_contained(),
106 location: self.location,
107 }
108 }
109}
110
111impl CssParseErrorOwned {
112 #[must_use] pub fn to_shared(&self) -> CssParseError<'_> {
113 CssParseError {
114 css_string: self.css_string.as_str(),
115 error: self.error.to_shared(),
116 location: self.location,
117 }
118 }
119}
120
121impl<'a> CssParseError<'a> {
122 #[must_use] pub fn get_error_string(&self) -> &'a str {
124 let (start, end) = (self.location.start.original_pos, self.location.end.original_pos);
125 let s = &self.css_string[start..end];
126 s.trim()
127 }
128}
129
130#[derive(Debug, Clone, PartialEq)]
131pub enum CssParseErrorInner<'a> {
132 ParseError(CssSyntaxError),
134 UnclosedBlock,
136 MalformedCss,
138 DynamicCssParseError(DynamicCssParseError<'a>),
141 PseudoSelectorParseError(CssPseudoSelectorParseError<'a>),
143 NodeTypeTag(NodeTypeTagParseError<'a>),
145 UnknownPropertyKey(&'a str, &'a str),
148 VarOnShorthandProperty {
154 key: CombinedCssPropertyType,
155 value: &'a str,
156 },
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
161#[repr(C)]
162pub struct UnknownPropertyKeyError {
163 pub key: AzString,
164 pub value: AzString,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
169#[repr(C)]
170pub struct VarOnShorthandPropertyError {
171 pub key: CombinedCssPropertyType,
172 pub value: AzString,
173}
174
175#[derive(Debug, Clone, PartialEq)]
176#[repr(C, u8)]
177pub enum CssParseErrorInnerOwned {
178 ParseError(CssSyntaxError),
179 UnclosedBlock,
180 MalformedCss,
181 DynamicCssParseError(DynamicCssParseErrorOwned),
182 PseudoSelectorParseError(CssPseudoSelectorParseErrorOwned),
183 NodeTypeTag(NodeTypeTagParseErrorOwned),
184 UnknownPropertyKey(UnknownPropertyKeyError),
185 VarOnShorthandProperty(VarOnShorthandPropertyError),
186}
187
188impl CssParseErrorInner<'_> {
189 #[must_use] pub fn to_contained(&self) -> CssParseErrorInnerOwned {
190 match self {
191 CssParseErrorInner::ParseError(e) => CssParseErrorInnerOwned::ParseError(*e),
192 CssParseErrorInner::UnclosedBlock => CssParseErrorInnerOwned::UnclosedBlock,
193 CssParseErrorInner::MalformedCss => CssParseErrorInnerOwned::MalformedCss,
194 CssParseErrorInner::DynamicCssParseError(e) => {
195 CssParseErrorInnerOwned::DynamicCssParseError(e.to_contained())
196 }
197 CssParseErrorInner::PseudoSelectorParseError(e) => {
198 CssParseErrorInnerOwned::PseudoSelectorParseError(e.to_contained())
199 }
200 CssParseErrorInner::NodeTypeTag(e) => {
201 CssParseErrorInnerOwned::NodeTypeTag(e.to_contained())
202 }
203 CssParseErrorInner::UnknownPropertyKey(a, b) => {
204 CssParseErrorInnerOwned::UnknownPropertyKey(UnknownPropertyKeyError { key: (*a).to_string().into(), value: (*b).to_string().into() })
205 }
206 CssParseErrorInner::VarOnShorthandProperty { key, value } => {
207 CssParseErrorInnerOwned::VarOnShorthandProperty(VarOnShorthandPropertyError {
208 key: *key,
209 value: (*value).to_string().into(),
210 })
211 }
212 }
213 }
214}
215
216impl CssParseErrorInnerOwned {
217 #[must_use] pub fn to_shared(&self) -> CssParseErrorInner<'_> {
218 match self {
219 Self::ParseError(e) => CssParseErrorInner::ParseError(*e),
220 Self::UnclosedBlock => CssParseErrorInner::UnclosedBlock,
221 Self::MalformedCss => CssParseErrorInner::MalformedCss,
222 Self::DynamicCssParseError(e) => {
223 CssParseErrorInner::DynamicCssParseError(e.to_shared())
224 }
225 Self::PseudoSelectorParseError(e) => {
226 CssParseErrorInner::PseudoSelectorParseError(e.to_shared())
227 }
228 Self::NodeTypeTag(e) => {
229 CssParseErrorInner::NodeTypeTag(e.to_shared())
230 }
231 Self::UnknownPropertyKey(e) => {
232 CssParseErrorInner::UnknownPropertyKey(e.key.as_str(), e.value.as_str())
233 }
234 Self::VarOnShorthandProperty(e) => {
235 CssParseErrorInner::VarOnShorthandProperty {
236 key: e.key,
237 value: e.value.as_str(),
238 }
239 }
240 }
241 }
242}
243
244impl_display! { CssParseErrorInner<'a>, {
245 ParseError(e) => format!("Parse Error: {:?}", e),
246 UnclosedBlock => "Unclosed block",
247 MalformedCss => "Malformed Css",
248 DynamicCssParseError(e) => format!("{}", e),
249 PseudoSelectorParseError(e) => format!("Failed to parse pseudo-selector: {}", e),
250 NodeTypeTag(e) => format!("Failed to parse CSS selector path: {}", e),
251 UnknownPropertyKey(k, v) => format!("Unknown CSS key: \"{}: {}\"", k, v),
252 VarOnShorthandProperty { key, value } => format!(
253 "Error while parsing: \"{}: {};\": var() cannot be used on shorthand properties - use `{}-top` or `{}-x` as the key instead: ",
254 key, value, key, key
255 ),
256}}
257
258impl From<CssSyntaxError> for CssParseErrorInner<'_> {
259 fn from(e: CssSyntaxError) -> Self {
260 CssParseErrorInner::ParseError(e)
261 }
262}
263
264impl From<SimplecssError> for CssParseErrorInner<'_> {
265 fn from(e: SimplecssError) -> Self {
266 CssParseErrorInner::ParseError(CssSyntaxError::from(e))
267 }
268}
269
270impl_from! { DynamicCssParseError<'a>, CssParseErrorInner::DynamicCssParseError }
271impl_from! { NodeTypeTagParseError<'a>, CssParseErrorInner::NodeTypeTag }
272impl_from! { CssPseudoSelectorParseError<'a>, CssParseErrorInner::PseudoSelectorParseError }
273
274#[derive(Debug, Clone, PartialEq, Eq)]
275pub enum CssPseudoSelectorParseError<'a> {
276 EmptyNthChild,
277 UnknownSelector(&'a str, Option<&'a str>),
278 InvalidNthChildPattern(&'a str),
279 InvalidNthChild(ParseIntError),
280}
281
282impl From<ParseIntError> for CssPseudoSelectorParseError<'_> {
283 fn from(e: ParseIntError) -> Self {
284 CssPseudoSelectorParseError::InvalidNthChild(e)
285 }
286}
287
288impl_display! { CssPseudoSelectorParseError<'a>, {
289 EmptyNthChild => format!("\
290 Empty :nth-child() selector - nth-child() must at least take a number, \
291 a pattern (such as \"2n+3\") or the values \"even\" or \"odd\"."
292 ),
293 UnknownSelector(selector, value) => {
294 let format_str = value
295 .as_ref()
296 .map_or_else(|| (*selector).to_string(), |v| format!("{selector}({v})"));
297 format!("Invalid or unknown CSS pseudo-selector: ':{format_str}'")
298 },
299 InvalidNthChildPattern(selector) => format!(
300 "Invalid pseudo-selector :{} - value has to be a \
301 number, \"even\" or \"odd\" or a pattern such as \"2n+3\"", selector
302 ),
303 InvalidNthChild(e) => format!("Invalid :nth-child pseudo-selector: ':{}'", e),
304}}
305
306#[derive(Debug, Clone, PartialEq, Eq)]
308#[repr(C)]
309pub struct UnknownSelectorError {
310 pub selector: AzString,
311 pub suggestion: OptionString,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
315#[repr(C, u8)]
316pub enum CssPseudoSelectorParseErrorOwned {
317 EmptyNthChild,
318 UnknownSelector(UnknownSelectorError),
319 InvalidNthChildPattern(AzString),
320 InvalidNthChild(crate::props::basic::error::ParseIntError),
321}
322
323impl CssPseudoSelectorParseError<'_> {
324 #[must_use] pub fn to_contained(&self) -> CssPseudoSelectorParseErrorOwned {
325 match self {
326 CssPseudoSelectorParseError::EmptyNthChild => {
327 CssPseudoSelectorParseErrorOwned::EmptyNthChild
328 }
329 CssPseudoSelectorParseError::UnknownSelector(a, b) => {
330 CssPseudoSelectorParseErrorOwned::UnknownSelector(UnknownSelectorError {
331 selector: (*a).to_string().into(),
332 suggestion: b.map(|s| AzString::from(s.to_string())).into(),
333 })
334 }
335 CssPseudoSelectorParseError::InvalidNthChildPattern(s) => {
336 CssPseudoSelectorParseErrorOwned::InvalidNthChildPattern((*s).to_string().into())
337 }
338 CssPseudoSelectorParseError::InvalidNthChild(e) => {
339 CssPseudoSelectorParseErrorOwned::InvalidNthChild(e.clone().into())
340 }
341 }
342 }
343}
344
345impl CssPseudoSelectorParseErrorOwned {
346 #[must_use] pub fn to_shared(&self) -> CssPseudoSelectorParseError<'_> {
347 match self {
348 Self::EmptyNthChild => {
349 CssPseudoSelectorParseError::EmptyNthChild
350 }
351 Self::UnknownSelector(e) => {
352 CssPseudoSelectorParseError::UnknownSelector(e.selector.as_str(), e.suggestion.as_ref().map(AzString::as_str))
353 }
354 Self::InvalidNthChildPattern(s) => {
355 CssPseudoSelectorParseError::InvalidNthChildPattern(s)
356 }
357 Self::InvalidNthChild(e) => {
358 CssPseudoSelectorParseError::InvalidNthChild(e.to_std())
359 }
360 }
361 }
362}
363
364#[derive(Debug, Clone, PartialEq)]
366pub enum DynamicCssParseError<'a> {
367 InvalidBraceContents(&'a str),
369 UnexpectedValue(CssParsingError<'a>),
371}
372
373impl_display! { DynamicCssParseError<'a>, {
374 InvalidBraceContents(e) => format!("Invalid contents of var() function: var({})", e),
375 UnexpectedValue(e) => format!("{}", e),
376}}
377
378impl<'a> From<CssParsingError<'a>> for DynamicCssParseError<'a> {
379 fn from(e: CssParsingError<'a>) -> Self {
380 DynamicCssParseError::UnexpectedValue(e)
381 }
382}
383
384#[derive(Debug, Clone, PartialEq)]
385#[repr(C, u8)]
386pub enum DynamicCssParseErrorOwned {
387 InvalidBraceContents(AzString),
388 UnexpectedValue(CssParsingErrorOwned),
389}
390
391impl DynamicCssParseError<'_> {
392 #[must_use] pub fn to_contained(&self) -> DynamicCssParseErrorOwned {
393 match self {
394 DynamicCssParseError::InvalidBraceContents(s) => {
395 DynamicCssParseErrorOwned::InvalidBraceContents((*s).to_string().into())
396 }
397 DynamicCssParseError::UnexpectedValue(e) => {
398 DynamicCssParseErrorOwned::UnexpectedValue(e.to_contained())
399 }
400 }
401 }
402}
403
404impl DynamicCssParseErrorOwned {
405 #[must_use] pub fn to_shared(&self) -> DynamicCssParseError<'_> {
406 match self {
407 Self::InvalidBraceContents(s) => {
408 DynamicCssParseError::InvalidBraceContents(s)
409 }
410 Self::UnexpectedValue(e) => {
411 DynamicCssParseError::UnexpectedValue(e.to_shared())
412 }
413 }
414 }
415}
416
417pub fn pseudo_selector_from_str<'a>(
423 selector: &'a str,
424 value: Option<&'a str>,
425) -> Result<CssPathPseudoSelector, CssPseudoSelectorParseError<'a>> {
426 match selector {
427 "first" => Ok(CssPathPseudoSelector::First),
428 "last" => Ok(CssPathPseudoSelector::Last),
429 "hover" => Ok(CssPathPseudoSelector::Hover),
430 "active" => Ok(CssPathPseudoSelector::Active),
431 "focus" => Ok(CssPathPseudoSelector::Focus),
432 "dragging" => Ok(CssPathPseudoSelector::Dragging),
433 "drag-over" => Ok(CssPathPseudoSelector::DragOver),
434 "nth-child" => {
435 let value = value.ok_or(CssPseudoSelectorParseError::EmptyNthChild)?;
436 let parsed = parse_nth_child_selector(value)?;
437 Ok(CssPathPseudoSelector::NthChild(parsed))
438 }
439 "lang" => {
440 let lang_value = value.ok_or(CssPseudoSelectorParseError::UnknownSelector(
441 selector, value,
442 ))?;
443 let lang_value = lang_value
445 .trim()
446 .trim_start_matches('"')
447 .trim_end_matches('"')
448 .trim_start_matches('\'')
449 .trim_end_matches('\'')
450 .trim();
451 Ok(CssPathPseudoSelector::Lang(AzString::from(
452 lang_value.to_string(),
453 )))
454 }
455 _ => Err(CssPseudoSelectorParseError::UnknownSelector(
456 selector, value,
457 )),
458 }
459}
460
461#[must_use] pub fn parse_attribute_selector(input: &str) -> Option<CssAttributeSelector> {
465 let s = input.trim();
466 if s.is_empty() {
467 return None;
468 }
469
470 let compound_ops: [(&str, AttributeMatchOp); 5] = [
473 ("~=", AttributeMatchOp::Includes),
474 ("|=", AttributeMatchOp::DashMatch),
475 ("^=", AttributeMatchOp::Prefix),
476 ("$=", AttributeMatchOp::Suffix),
477 ("*=", AttributeMatchOp::Substring),
478 ];
479 let (op, op_pos): (AttributeMatchOp, Option<usize>) = compound_ops
480 .iter()
481 .find_map(|(pat, op)| s.find(pat).map(|i| (*op, Some(i))))
482 .or_else(|| s.find('=').map(|i| (AttributeMatchOp::Eq, Some(i))))
483 .unwrap_or((AttributeMatchOp::Exists, None));
484
485 let (name, value) = match op_pos {
486 None => (s, None),
487 Some(i) => {
488 let name = s[..i].trim();
489 let op_len = if matches!(op, AttributeMatchOp::Eq) { 1 } else { 2 };
490 let raw_value = s[i + op_len..].trim();
491 let unquoted = strip_attribute_quotes(raw_value)?;
492 (name, Some(unquoted))
493 }
494 };
495
496 if name.is_empty() {
497 return None;
498 }
499 if name.chars().any(|c| c.is_whitespace() || c == '"' || c == '\'') {
501 return None;
502 }
503
504 Some(CssAttributeSelector {
505 name: name.to_string().into(),
506 op,
507 value: value
508 .map_or_else(|| OptionString::None, |v| OptionString::Some(v.to_string().into())),
509 })
510}
511
512fn strip_attribute_quotes(s: &str) -> Option<&str> {
515 let bytes = s.as_bytes();
516 if bytes.len() >= 2 {
517 let first = bytes[0];
518 let last = bytes[bytes.len() - 1];
519 if (first == b'"' && last == b'"') || (first == b'\'' && last == b'\'') {
520 return Some(&s[1..s.len() - 1]);
521 }
522 if first == b'"' || first == b'\'' || last == b'"' || last == b'\'' {
523 return None;
525 }
526 } else if bytes.len() == 1 && (bytes[0] == b'"' || bytes[0] == b'\'') {
527 return None;
528 }
529 Some(s)
530}
531
532fn parse_nth_child_selector(
536 value: &str,
537) -> Result<CssNthChildSelector, CssPseudoSelectorParseError<'_>> {
538 let value = value.trim();
539
540 if value.is_empty() {
541 return Err(CssPseudoSelectorParseError::EmptyNthChild);
542 }
543
544 if let Ok(number) = value.parse::<u32>() {
545 return Ok(CssNthChildSelector::Number(number));
546 }
547
548 match value {
550 "even" => Ok(CssNthChildSelector::Even),
551 "odd" => Ok(CssNthChildSelector::Odd),
552 _ => parse_nth_child_pattern(value),
553 }
554}
555
556fn parse_nth_child_pattern(
558 value: &str,
559) -> Result<CssNthChildSelector, CssPseudoSelectorParseError<'_>> {
560 use crate::css::CssNthChildPattern;
561
562 let value = value.trim();
563
564 if value.is_empty() {
565 return Err(CssPseudoSelectorParseError::EmptyNthChild);
566 }
567
568 let repeat = value
570 .split('n')
571 .next()
572 .ok_or(CssPseudoSelectorParseError::InvalidNthChildPattern(value))?
573 .trim()
574 .parse::<u32>()?;
575
576 let mut offset_iterator = value.split('+');
578
579 offset_iterator.next().unwrap();
581
582 let offset = match offset_iterator.next() {
583 Some(offset_string) => {
584 let offset_string = offset_string.trim();
585 if offset_string.is_empty() {
586 return Err(CssPseudoSelectorParseError::InvalidNthChildPattern(value));
587 }
588 offset_string.parse::<u32>()?
589 }
590 None => 0,
591 };
592
593 Ok(CssNthChildSelector::Pattern(CssNthChildPattern {
594 pattern_repeat: repeat,
595 offset,
596 }))
597}
598
599#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
600#[repr(C)]
601pub struct ErrorLocation {
602 pub original_pos: usize,
603}
604
605#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
608#[repr(C)]
609pub struct ErrorLocationRange {
610 pub start: ErrorLocation,
611 pub end: ErrorLocation,
612}
613
614impl ErrorLocation {
615 #[must_use] pub fn get_line_column_from_error(&self, css_string: &str) -> (usize, usize) {
617 let error_location = self.original_pos.saturating_sub(1);
618 let (mut line_number, mut total_characters) = (0, 0);
619
620 for line in css_string[0..error_location].lines() {
621 line_number += 1;
622 total_characters += line.chars().count();
623 }
624
625 let total_characters = total_characters + line_number;
627 let column_pos = error_location - total_characters.saturating_sub(2);
628
629 (line_number, column_pos)
630 }
631}
632
633impl fmt::Display for CssParseError<'_> {
634 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
635 let start_location = self.location.start.get_line_column_from_error(self.css_string);
636 let end_location = self.location.end.get_line_column_from_error(self.css_string);
637 write!(
638 f,
639 " start: line {}:{}\r\n end: line {}:{}\r\n text: \"{}\"\r\n reason: {}",
640 start_location.0,
641 start_location.1,
642 end_location.0,
643 end_location.1,
644 self.get_error_string(),
645 self.error,
646 )
647 }
648}
649
650#[must_use] pub fn new_from_str(css_string: &str) -> (Css, Vec<CssParseWarnMsg<'_>>) {
656 let mut tokenizer = Tokenizer::new(css_string);
657 let (rules, warnings) = new_from_str_inner(css_string, &mut tokenizer);
658
659 (
660 Css { rules: rules.into() },
661 warnings,
662 )
663}
664
665fn get_error_location(tokenizer: &Tokenizer<'_>) -> ErrorLocation {
667 ErrorLocation {
668 original_pos: tokenizer.pos(),
669 }
670}
671
672#[derive(Debug, Clone, PartialEq, Eq)]
673pub enum CssPathParseError<'a> {
674 EmptyPath,
675 InvalidTokenEncountered(&'a str),
677 UnexpectedEndOfStream(&'a str),
678 SyntaxError(CssSyntaxError),
679 NodeTypeTag(NodeTypeTagParseError<'a>),
681 PseudoSelectorParseError(CssPseudoSelectorParseError<'a>),
683}
684
685impl_from! { NodeTypeTagParseError<'a>, CssPathParseError::NodeTypeTag }
686impl_from! { CssPseudoSelectorParseError<'a>, CssPathParseError::PseudoSelectorParseError }
687
688impl From<CssSyntaxError> for CssPathParseError<'_> {
689 fn from(e: CssSyntaxError) -> Self {
690 CssPathParseError::SyntaxError(e)
691 }
692}
693
694impl From<SimplecssError> for CssPathParseError<'_> {
695 fn from(e: SimplecssError) -> Self {
696 CssPathParseError::SyntaxError(CssSyntaxError::from(e))
697 }
698}
699
700#[derive(Debug, Clone, PartialEq, Eq)]
701pub enum CssPathParseErrorOwned {
702 EmptyPath,
703 InvalidTokenEncountered(AzString),
704 UnexpectedEndOfStream(AzString),
705 SyntaxError(CssSyntaxError),
706 NodeTypeTag(NodeTypeTagParseErrorOwned),
707 PseudoSelectorParseError(CssPseudoSelectorParseErrorOwned),
708}
709
710impl CssPathParseError<'_> {
711 #[must_use] pub fn to_contained(&self) -> CssPathParseErrorOwned {
712 match self {
713 CssPathParseError::EmptyPath => CssPathParseErrorOwned::EmptyPath,
714 CssPathParseError::InvalidTokenEncountered(s) => {
715 CssPathParseErrorOwned::InvalidTokenEncountered((*s).to_string().into())
716 }
717 CssPathParseError::UnexpectedEndOfStream(s) => {
718 CssPathParseErrorOwned::UnexpectedEndOfStream((*s).to_string().into())
719 }
720 CssPathParseError::SyntaxError(e) => CssPathParseErrorOwned::SyntaxError(*e),
721 CssPathParseError::NodeTypeTag(e) => {
722 CssPathParseErrorOwned::NodeTypeTag(e.to_contained())
723 }
724 CssPathParseError::PseudoSelectorParseError(e) => {
725 CssPathParseErrorOwned::PseudoSelectorParseError(e.to_contained())
726 }
727 }
728 }
729}
730
731impl CssPathParseErrorOwned {
732 #[must_use] pub fn to_shared(&self) -> CssPathParseError<'_> {
733 match self {
734 Self::EmptyPath => CssPathParseError::EmptyPath,
735 Self::InvalidTokenEncountered(s) => {
736 CssPathParseError::InvalidTokenEncountered(s)
737 }
738 Self::UnexpectedEndOfStream(s) => {
739 CssPathParseError::UnexpectedEndOfStream(s)
740 }
741 Self::SyntaxError(e) => CssPathParseError::SyntaxError(*e),
742 Self::NodeTypeTag(e) => CssPathParseError::NodeTypeTag(e.to_shared()),
743 Self::PseudoSelectorParseError(e) => {
744 CssPathParseError::PseudoSelectorParseError(e.to_shared())
745 }
746 }
747 }
748}
749
750pub fn parse_css_path(input: &str) -> Result<CssPath, CssPathParseError<'_>> {
780 use azul_simplecss::{Combinator, Token};
781
782 let input = input.trim();
783 if input.is_empty() {
784 return Err(CssPathParseError::EmptyPath);
785 }
786
787 let mut tokenizer = Tokenizer::new(input);
788 let mut selectors = Vec::new();
789
790 loop {
791 let token = tokenizer.parse_next()?;
792 match token {
793 Token::UniversalSelector => {
794 selectors.push(CssPathSelector::Global);
795 }
796 Token::TypeSelector(div_type) => {
797 if let Ok(nt) = NodeTypeTag::from_str(div_type) {
798 selectors.push(CssPathSelector::Type(nt));
799 }
800 }
801 Token::IdSelector(id) => {
802 selectors.push(CssPathSelector::Id(id.to_string().into()));
803 }
804 Token::ClassSelector(class) => {
805 selectors.push(CssPathSelector::Class(class.to_string().into()));
806 }
807 Token::Combinator(Combinator::GreaterThan) => {
808 selectors.push(CssPathSelector::DirectChildren);
809 }
810 Token::Combinator(Combinator::Space) => {
811 selectors.push(CssPathSelector::Children);
812 }
813 Token::Combinator(Combinator::Plus) => {
814 selectors.push(CssPathSelector::AdjacentSibling);
815 }
816 Token::Combinator(Combinator::Tilde) => {
817 selectors.push(CssPathSelector::GeneralSibling);
818 }
819 Token::PseudoClass { selector, value } => {
820 selectors.push(CssPathSelector::PseudoSelector(pseudo_selector_from_str(
821 selector, value,
822 )?));
823 }
824 Token::EndOfStream => {
825 break;
826 }
827 _ => {
828 return Err(CssPathParseError::InvalidTokenEncountered(input));
829 }
830 }
831 }
832
833 if selectors.is_empty() {
834 Err(CssPathParseError::EmptyPath)
835 } else {
836 Ok(CssPath {
837 selectors: selectors.into(),
838 })
839 }
840}
841
842#[derive(Debug, Clone, PartialEq, Eq)]
843pub struct UnparsedCssRuleBlock<'a> {
844 pub path: CssPath,
846 pub declarations: BTreeMap<&'a str, (&'a str, ErrorLocationRange)>,
848 pub conditions: Vec<DynamicSelector>,
850}
851
852#[derive(Debug, Clone, PartialEq, Eq)]
854pub struct UnparsedCssRuleBlockOwned {
855 pub path: CssPath,
856 pub declarations: BTreeMap<String, (String, ErrorLocationRange)>,
857 pub conditions: Vec<DynamicSelector>,
858}
859
860impl UnparsedCssRuleBlock<'_> {
861 #[must_use] pub fn to_contained(&self) -> UnparsedCssRuleBlockOwned {
862 UnparsedCssRuleBlockOwned {
863 path: self.path.clone(),
864 declarations: self
865 .declarations
866 .iter()
867 .map(|(k, (v, loc))| ((*k).to_string(), ((*v).to_string(), *loc)))
868 .collect(),
869 conditions: self.conditions.clone(),
870 }
871 }
872}
873
874impl UnparsedCssRuleBlockOwned {
875 #[must_use] pub fn to_shared(&self) -> UnparsedCssRuleBlock<'_> {
876 UnparsedCssRuleBlock {
877 path: self.path.clone(),
878 declarations: self
879 .declarations
880 .iter()
881 .map(|(k, (v, loc))| (k.as_str(), (v.as_str(), *loc)))
882 .collect(),
883 conditions: self.conditions.clone(),
884 }
885 }
886}
887
888#[derive(Debug, Clone, PartialEq)]
889pub struct CssParseWarnMsg<'a> {
890 pub warning: CssParseWarnMsgInner<'a>,
891 pub location: ErrorLocationRange,
892}
893
894#[derive(Debug, Clone, PartialEq)]
896pub struct CssParseWarnMsgOwned {
897 pub warning: CssParseWarnMsgInnerOwned,
898 pub location: ErrorLocationRange,
899}
900
901impl CssParseWarnMsg<'_> {
902 #[must_use] pub fn to_contained(&self) -> CssParseWarnMsgOwned {
903 CssParseWarnMsgOwned {
904 warning: self.warning.to_contained(),
905 location: self.location,
906 }
907 }
908}
909
910impl CssParseWarnMsgOwned {
911 #[must_use] pub fn to_shared(&self) -> CssParseWarnMsg<'_> {
912 CssParseWarnMsg {
913 warning: self.warning.to_shared(),
914 location: self.location,
915 }
916 }
917}
918
919#[derive(Debug, Clone, PartialEq)]
920pub enum CssParseWarnMsgInner<'a> {
921 UnsupportedKeyValuePair { key: &'a str, value: &'a str },
923 ParseError(CssParseErrorInner<'a>),
925 SkippedRule {
927 selector: Option<&'a str>,
928 error: CssParseErrorInner<'a>,
929 },
930 SkippedDeclaration {
932 key: &'a str,
933 value: &'a str,
934 error: CssParseErrorInner<'a>,
935 },
936 MalformedStructure { message: &'a str },
938}
939
940#[derive(Debug, Clone, PartialEq)]
941pub enum CssParseWarnMsgInnerOwned {
942 UnsupportedKeyValuePair {
943 key: String,
944 value: String,
945 },
946 ParseError(CssParseErrorInnerOwned),
947 SkippedRule {
948 selector: Option<String>,
949 error: CssParseErrorInnerOwned,
950 },
951 SkippedDeclaration {
952 key: String,
953 value: String,
954 error: CssParseErrorInnerOwned,
955 },
956 MalformedStructure {
957 message: String,
958 },
959}
960
961impl CssParseWarnMsgInner<'_> {
962 #[must_use] pub fn to_contained(&self) -> CssParseWarnMsgInnerOwned {
963 match self {
964 Self::UnsupportedKeyValuePair { key, value } => {
965 CssParseWarnMsgInnerOwned::UnsupportedKeyValuePair {
966 key: (*key).to_string(),
967 value: (*value).to_string(),
968 }
969 }
970 Self::ParseError(e) => CssParseWarnMsgInnerOwned::ParseError(e.to_contained()),
971 Self::SkippedRule { selector, error } => CssParseWarnMsgInnerOwned::SkippedRule {
972 selector: selector.map(std::string::ToString::to_string),
973 error: error.to_contained(),
974 },
975 Self::SkippedDeclaration { key, value, error } => {
976 CssParseWarnMsgInnerOwned::SkippedDeclaration {
977 key: (*key).to_string(),
978 value: (*value).to_string(),
979 error: error.to_contained(),
980 }
981 }
982 Self::MalformedStructure { message } => CssParseWarnMsgInnerOwned::MalformedStructure {
983 message: (*message).to_string(),
984 },
985 }
986 }
987}
988
989impl CssParseWarnMsgInnerOwned {
990 #[must_use] pub fn to_shared(&self) -> CssParseWarnMsgInner<'_> {
991 match self {
992 Self::UnsupportedKeyValuePair { key, value } => {
993 CssParseWarnMsgInner::UnsupportedKeyValuePair { key, value }
994 }
995 Self::ParseError(e) => CssParseWarnMsgInner::ParseError(e.to_shared()),
996 Self::SkippedRule { selector, error } => CssParseWarnMsgInner::SkippedRule {
997 selector: selector.as_deref(),
998 error: error.to_shared(),
999 },
1000 Self::SkippedDeclaration { key, value, error } => {
1001 CssParseWarnMsgInner::SkippedDeclaration {
1002 key,
1003 value,
1004 error: error.to_shared(),
1005 }
1006 }
1007 Self::MalformedStructure { message } => {
1008 CssParseWarnMsgInner::MalformedStructure { message }
1009 }
1010 }
1011 }
1012}
1013
1014impl_display! { CssParseWarnMsgInner<'a>, {
1015 UnsupportedKeyValuePair { key, value } => format!("Unsupported CSS property: \"{}: {}\"", key, value),
1016 ParseError(e) => format!("Parse error (recoverable): {}", e),
1017 SkippedRule { selector, error } => {
1018 let sel = selector.unwrap_or("unknown");
1019 format!("Skipped rule for selector '{sel}': {error}")
1020 },
1021 SkippedDeclaration { key, value, error } => format!("Skipped declaration '{}:{}': {}", key, value, error),
1022 MalformedStructure { message } => format!("Malformed CSS structure: {}", message),
1023}}
1024
1025fn parse_media_conditions(content: &str) -> Vec<DynamicSelector> {
1028 let mut conditions = Vec::new();
1029 let content = content.trim();
1030
1031 if content.eq_ignore_ascii_case("screen") {
1033 conditions.push(DynamicSelector::Media(MediaType::Screen));
1034 return conditions;
1035 }
1036 if content.eq_ignore_ascii_case("print") {
1037 conditions.push(DynamicSelector::Media(MediaType::Print));
1038 return conditions;
1039 }
1040 if content.eq_ignore_ascii_case("all") {
1041 conditions.push(DynamicSelector::Media(MediaType::All));
1042 return conditions;
1043 }
1044
1045 for part in content.split(" and ") {
1048 let part = part.trim();
1049
1050 if part.eq_ignore_ascii_case("screen")
1052 || part.eq_ignore_ascii_case("print")
1053 || part.eq_ignore_ascii_case("all")
1054 {
1055 if part.eq_ignore_ascii_case("screen") {
1056 conditions.push(DynamicSelector::Media(MediaType::Screen));
1057 } else if part.eq_ignore_ascii_case("print") {
1058 conditions.push(DynamicSelector::Media(MediaType::Print));
1059 } else if part.eq_ignore_ascii_case("all") {
1060 conditions.push(DynamicSelector::Media(MediaType::All));
1061 }
1062 continue;
1063 }
1064
1065 if let Some(inner) = part.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
1067 if let Some(selector) = parse_media_feature(inner) {
1068 conditions.push(selector);
1069 }
1070 }
1071 }
1072
1073 conditions
1074}
1075
1076fn parse_media_feature(feature: &str) -> Option<DynamicSelector> {
1078 let parts: Vec<&str> = feature.splitn(2, ':').collect();
1079 if parts.len() != 2 {
1080 return None;
1082 }
1083
1084 let key = parts[0].trim();
1085 let value = parts[1].trim();
1086
1087 match key.to_lowercase().as_str() {
1088 "min-width" => {
1089 if let Some(px) = parse_px_value(value) {
1090 return Some(DynamicSelector::ViewportWidth(MinMaxRange::new(
1091 Some(px),
1092 None,
1093 )));
1094 }
1095 }
1096 "max-width" => {
1097 if let Some(px) = parse_px_value(value) {
1098 return Some(DynamicSelector::ViewportWidth(MinMaxRange::new(
1099 None,
1100 Some(px),
1101 )));
1102 }
1103 }
1104 "min-height" => {
1105 if let Some(px) = parse_px_value(value) {
1106 return Some(DynamicSelector::ViewportHeight(MinMaxRange::new(
1107 Some(px),
1108 None,
1109 )));
1110 }
1111 }
1112 "max-height" => {
1113 if let Some(px) = parse_px_value(value) {
1114 return Some(DynamicSelector::ViewportHeight(MinMaxRange::new(
1115 None,
1116 Some(px),
1117 )));
1118 }
1119 }
1120 "orientation" => {
1121 if value.eq_ignore_ascii_case("portrait") {
1122 return Some(DynamicSelector::Orientation(OrientationType::Portrait));
1123 } else if value.eq_ignore_ascii_case("landscape") {
1124 return Some(DynamicSelector::Orientation(OrientationType::Landscape));
1125 }
1126 }
1127 "prefers-color-scheme" => {
1128 if value.eq_ignore_ascii_case("dark") {
1129 return Some(DynamicSelector::Theme(ThemeCondition::Dark));
1130 } else if value.eq_ignore_ascii_case("light") {
1131 return Some(DynamicSelector::Theme(ThemeCondition::Light));
1132 }
1133 }
1134 "prefers-reduced-motion" => {
1135 if value.eq_ignore_ascii_case("reduce") {
1136 return Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True));
1137 } else if value.eq_ignore_ascii_case("no-preference") {
1138 return Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False));
1139 }
1140 }
1141 "prefers-contrast" | "prefers-high-contrast" => {
1142 if value.eq_ignore_ascii_case("more") || value.eq_ignore_ascii_case("high") || value.eq_ignore_ascii_case("active") {
1143 return Some(DynamicSelector::PrefersHighContrast(BoolCondition::True));
1144 } else if value.eq_ignore_ascii_case("no-preference") || value.eq_ignore_ascii_case("none") {
1145 return Some(DynamicSelector::PrefersHighContrast(BoolCondition::False));
1146 }
1147 }
1148 "aspect-ratio" => {
1149 if let Some(ratio) = parse_ratio_value(value) {
1150 return Some(DynamicSelector::AspectRatio(MinMaxRange::new(Some(ratio), Some(ratio))));
1151 }
1152 }
1153 "min-aspect-ratio" => {
1154 if let Some(ratio) = parse_ratio_value(value) {
1155 return Some(DynamicSelector::AspectRatio(MinMaxRange::new(Some(ratio), None)));
1156 }
1157 }
1158 "max-aspect-ratio" => {
1159 if let Some(ratio) = parse_ratio_value(value) {
1160 return Some(DynamicSelector::AspectRatio(MinMaxRange::new(None, Some(ratio))));
1161 }
1162 }
1163 _ => {}
1164 }
1165
1166 None
1167}
1168
1169fn parse_px_value(value: &str) -> Option<f32> {
1171 let value = value.trim();
1172 value.strip_suffix("px").map_or_else(
1173 || value.parse::<f32>().ok(),
1175 |num_str| num_str.trim().parse::<f32>().ok(),
1176 )
1177}
1178
1179fn parse_ratio_value(value: &str) -> Option<f32> {
1181 let value = value.trim();
1182 if let Some((num, den)) = value.split_once('/') {
1183 let num: f32 = num.trim().parse().ok()?;
1184 let den: f32 = den.trim().parse().ok()?;
1185 if den == 0.0 { return None; }
1186 Some(num / den)
1187 } else {
1188 value.parse::<f32>().ok()
1189 }
1190}
1191
1192fn parse_container_conditions(content: &str) -> Vec<DynamicSelector> {
1195 let mut conditions = Vec::new();
1196 let content = content.trim();
1197
1198 let (name_part, query_part) = if content.starts_with('(') {
1201 (None, content)
1202 } else if let Some(paren_idx) = content.find('(') {
1203 let name = content[..paren_idx].trim();
1204 if name.is_empty() {
1205 (None, content)
1206 } else {
1207 (Some(name), &content[paren_idx..])
1208 }
1209 } else {
1210 if !content.is_empty() {
1212 conditions.push(DynamicSelector::ContainerName(AzString::from(content.to_string())));
1213 }
1214 return conditions;
1215 };
1216
1217 if let Some(name) = name_part {
1218 conditions.push(DynamicSelector::ContainerName(AzString::from(name.to_string())));
1219 }
1220
1221 for part in query_part.split(" and ") {
1223 let part = part.trim();
1224 if let Some(inner) = part.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
1225 if let Some(selector) = parse_container_feature(inner) {
1226 conditions.push(selector);
1227 }
1228 }
1229 }
1230
1231 conditions
1232}
1233
1234fn parse_container_feature(feature: &str) -> Option<DynamicSelector> {
1236 let (key, value) = feature.split_once(':')?;
1237 let key = key.trim();
1238 let value = value.trim();
1239
1240 match key.to_lowercase().as_str() {
1241 "min-width" => {
1242 parse_px_value(value).map(|px| DynamicSelector::ContainerWidth(MinMaxRange::new(Some(px), None)))
1243 }
1244 "max-width" => {
1245 parse_px_value(value).map(|px| DynamicSelector::ContainerWidth(MinMaxRange::new(None, Some(px))))
1246 }
1247 "min-height" => {
1248 parse_px_value(value).map(|px| DynamicSelector::ContainerHeight(MinMaxRange::new(Some(px), None)))
1249 }
1250 "max-height" => {
1251 parse_px_value(value).map(|px| DynamicSelector::ContainerHeight(MinMaxRange::new(None, Some(px))))
1252 }
1253 "aspect-ratio" => {
1254 parse_ratio_value(value).map(|r| DynamicSelector::AspectRatio(MinMaxRange::new(Some(r), Some(r))))
1255 }
1256 "min-aspect-ratio" => {
1257 parse_ratio_value(value).map(|r| DynamicSelector::AspectRatio(MinMaxRange::new(Some(r), None)))
1258 }
1259 "max-aspect-ratio" => {
1260 parse_ratio_value(value).map(|r| DynamicSelector::AspectRatio(MinMaxRange::new(None, Some(r))))
1261 }
1262 _ => None,
1263 }
1264}
1265
1266fn parse_theme_condition(content: &str) -> Option<DynamicSelector> {
1269 let content = content.trim();
1270 let inner = content
1271 .strip_prefix('(')
1272 .and_then(|s| s.strip_suffix(')'))
1273 .unwrap_or(content)
1274 .trim();
1275 let inner = inner
1276 .strip_prefix('"')
1277 .and_then(|s| s.strip_suffix('"'))
1278 .or_else(|| inner.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1279 .unwrap_or(inner)
1280 .trim();
1281
1282 match inner.to_lowercase().as_str() {
1283 "dark" => Some(DynamicSelector::Theme(ThemeCondition::Dark)),
1284 "light" => Some(DynamicSelector::Theme(ThemeCondition::Light)),
1285 _ => None,
1286 }
1287}
1288
1289fn parse_lang_condition(content: &str) -> Option<DynamicSelector> {
1292 let content = content.trim();
1293
1294 let lang = content
1296 .strip_prefix('(')
1297 .and_then(|s| s.strip_suffix(')'))
1298 .unwrap_or(content)
1299 .trim();
1300
1301 let lang = lang
1302 .strip_prefix('"')
1303 .and_then(|s| s.strip_suffix('"'))
1304 .or_else(|| lang.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1305 .unwrap_or(lang)
1306 .trim();
1307
1308 if lang.is_empty() {
1309 return None;
1310 }
1311
1312 Some(DynamicSelector::Language(LanguageCondition::Prefix(
1314 AzString::from(lang.to_string()),
1315 )))
1316}
1317
1318#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn new_from_str_inner<'a>(
1325 css_string: &'a str,
1326 tokenizer: &mut Tokenizer<'a>,
1327) -> (Vec<CssRuleBlock>, Vec<CssParseWarnMsg<'a>>) {
1328 use azul_simplecss::{Combinator, Token};
1329
1330 struct NestingLevel<'a> {
1333 paths: Vec<Vec<CssPathSelector>>,
1334 declarations: BTreeMap<&'a str, (&'a str, ErrorLocationRange)>,
1335 depth: usize,
1336 }
1337
1338 fn get_parent_paths(nesting_stack: &[NestingLevel<'_>]) -> Vec<Vec<CssPathSelector>> {
1340 nesting_stack
1341 .last()
1342 .map_or_else(Vec::new, |parent| parent.paths.clone())
1343 }
1344
1345 fn combine_paths(
1349 parent_paths: &[Vec<CssPathSelector>],
1350 child_path: &[CssPathSelector],
1351 is_pseudo_only: bool,
1352 ) -> Vec<Vec<CssPathSelector>> {
1353 if parent_paths.is_empty() {
1354 vec![child_path.to_vec()]
1355 } else {
1356 parent_paths
1357 .iter()
1358 .map(|parent| {
1359 let mut combined = parent.clone();
1360 if !is_pseudo_only && !child_path.is_empty() {
1361 combined.push(CssPathSelector::Children);
1363 }
1364 combined.extend(child_path.iter().cloned());
1365 combined
1366 })
1367 .collect()
1368 }
1369 }
1370
1371 let mut css_blocks = Vec::new();
1372 let mut warnings = Vec::new();
1373
1374 let mut block_nesting = 0_usize;
1375 let mut last_path: Vec<CssPathSelector> = Vec::new();
1376 let mut last_error_location = ErrorLocation { original_pos: 0 };
1377
1378 let mut at_rule_stack: Vec<(Vec<DynamicSelector>, usize)> = Vec::new();
1381 let mut pending_at_rule: Option<&str> = None;
1383 let mut pending_at_str_parts: Vec<String> = Vec::new();
1385
1386 let mut nesting_stack: Vec<NestingLevel<'a>> = Vec::new();
1391 let mut current_paths: Vec<Vec<CssPathSelector>> = Vec::new();
1393 let mut current_declarations: BTreeMap<&str, (&str, ErrorLocationRange)> = BTreeMap::new();
1395
1396 let max_iterations = css_string.len().saturating_mul(10).max(1000);
1399 let mut iterations = 0_usize;
1400 let mut last_position = 0_usize;
1401 let mut stuck_count = 0_usize;
1402
1403 loop {
1404 iterations += 1;
1406 if iterations > max_iterations {
1407 warnings.push(CssParseWarnMsg {
1408 warning: CssParseWarnMsgInner::MalformedStructure {
1409 message: "Parser iteration limit exceeded - possible infinite loop",
1410 },
1411 location: ErrorLocationRange { start: last_error_location, end: get_error_location(tokenizer) },
1412 });
1413 break;
1414 }
1415
1416 let current_position = tokenizer.pos();
1418 if current_position == last_position {
1419 stuck_count += 1;
1420 if stuck_count > 10 {
1421 warnings.push(CssParseWarnMsg {
1422 warning: CssParseWarnMsgInner::MalformedStructure {
1423 message: "Parser stuck - position not advancing",
1424 },
1425 location: ErrorLocationRange { start: last_error_location, end: get_error_location(tokenizer) },
1426 });
1427 break;
1428 }
1429 } else {
1430 stuck_count = 0;
1431 last_position = current_position;
1432 }
1433
1434 let token = match tokenizer.parse_next() {
1435 Ok(token) => token,
1436 Err(e) => {
1437 let error_location = get_error_location(tokenizer);
1438 warnings.push(CssParseWarnMsg {
1439 warning: CssParseWarnMsgInner::ParseError(e.into()),
1440 location: ErrorLocationRange { start: last_error_location, end: error_location },
1441 });
1442 break;
1444 }
1445 };
1446
1447 macro_rules! warn_and_continue {
1448 ($warning:expr) => {{
1449 warnings.push(CssParseWarnMsg {
1450 warning: $warning,
1451 location: ErrorLocationRange { start: last_error_location, end: get_error_location(tokenizer) },
1452 });
1453 continue;
1454 }};
1455 }
1456
1457 match token {
1458 Token::AtRule(rule_name) => {
1459 pending_at_rule = Some(rule_name);
1461 pending_at_str_parts.clear();
1462 }
1463 Token::AtStr(content) => {
1464 if pending_at_rule.is_some() {
1466 if !content.eq_ignore_ascii_case("and") {
1468 pending_at_str_parts.push(content.to_string());
1469 }
1470 }
1471 }
1472 Token::BlockStart => {
1473 if let Some(rule_name) = pending_at_rule.take() {
1475 let combined_content = pending_at_str_parts.join(" and ");
1476 pending_at_str_parts.clear();
1477
1478 let conditions = match rule_name.to_lowercase().as_str() {
1479 "media" => parse_media_conditions(&combined_content),
1480 "lang" => parse_lang_condition(&combined_content).into_iter().collect(),
1481 "os" => crate::dynamic_selector::parse_os_at_rule_content(&combined_content).unwrap_or_default(),
1482 "theme" => parse_theme_condition(&combined_content).into_iter().collect(),
1483 "container" => parse_container_conditions(&combined_content),
1484 _ => {
1485 Vec::new()
1487 }
1488 };
1489
1490 if !conditions.is_empty() {
1491 at_rule_stack.push((conditions, block_nesting + 1));
1493 }
1494 }
1495
1496 block_nesting += 1;
1497
1498 if !current_paths.is_empty() || !last_path.is_empty() {
1500 if !last_path.is_empty() {
1502 current_paths.push(last_path.clone());
1503 last_path.clear();
1504 }
1505
1506 let parent_paths = get_parent_paths(&nesting_stack);
1508 let combined_paths: Vec<Vec<CssPathSelector>> = if parent_paths.is_empty() {
1509 current_paths.clone()
1510 } else {
1511 let mut result = Vec::new();
1513 for parent in &parent_paths {
1514 for child in ¤t_paths {
1515 let is_pseudo_only = child.first().is_some_and(|s| matches!(s, CssPathSelector::PseudoSelector(_)));
1517 let mut combined = parent.clone();
1518 if !is_pseudo_only && !child.is_empty() {
1519 combined.push(CssPathSelector::Children);
1520 }
1521 combined.extend(child.iter().cloned());
1522 result.push(combined);
1523 }
1524 }
1525 result
1526 };
1527
1528 nesting_stack.push(NestingLevel {
1530 paths: combined_paths,
1531 declarations: std::mem::take(&mut current_declarations),
1532 depth: block_nesting,
1533 });
1534 current_paths.clear();
1535 }
1536 }
1537 Token::Comma => {
1538 if !last_path.is_empty() {
1540 current_paths.push(last_path.clone());
1541 last_path.clear();
1542 }
1543 }
1544 Token::BlockEnd => {
1545 if block_nesting == 0 {
1546 warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1547 message: "Block end without matching block start"
1548 });
1549 }
1550
1551 let current_conditions: Vec<DynamicSelector> = at_rule_stack
1553 .iter()
1554 .flat_map(|(conds, _)| conds.iter().cloned())
1555 .collect();
1556
1557 while let Some((_, level)) = at_rule_stack.last() {
1559 if *level >= block_nesting {
1560 at_rule_stack.pop();
1561 } else {
1562 break;
1563 }
1564 }
1565
1566 block_nesting = block_nesting.saturating_sub(1);
1567
1568 if let Some(level) = nesting_stack.pop() {
1570 if !level.paths.is_empty() && !current_declarations.is_empty() {
1572 css_blocks.extend(level.paths.iter().map(|path| UnparsedCssRuleBlock {
1573 path: CssPath {
1574 selectors: path.clone().into(),
1575 },
1576 declarations: current_declarations.clone(),
1577 conditions: current_conditions.clone(),
1578 }));
1579 }
1580 current_declarations = level.declarations;
1582 }
1583
1584 last_path.clear();
1585 current_paths.clear();
1586 }
1587 Token::UniversalSelector => {
1588 last_path.push(CssPathSelector::Global);
1589 }
1590 Token::TypeSelector(div_type) => {
1591 match NodeTypeTag::from_str(div_type) {
1592 Ok(nt) => last_path.push(CssPathSelector::Type(nt)),
1593 Err(e) => {
1594 warn_and_continue!(CssParseWarnMsgInner::SkippedRule {
1595 selector: Some(div_type),
1596 error: e.into(),
1597 });
1598 }
1599 }
1600 }
1601 Token::IdSelector(id) => {
1602 last_path.push(CssPathSelector::Id(id.to_string().into()));
1603 }
1604 Token::ClassSelector(class) => {
1605 last_path.push(CssPathSelector::Class(class.to_string().into()));
1606 }
1607 Token::Combinator(Combinator::GreaterThan) => {
1608 last_path.push(CssPathSelector::DirectChildren);
1609 }
1610 Token::Combinator(Combinator::Space) => {
1611 last_path.push(CssPathSelector::Children);
1612 }
1613 Token::Combinator(Combinator::Plus) => {
1614 last_path.push(CssPathSelector::AdjacentSibling);
1615 }
1616 Token::Combinator(Combinator::Tilde) => {
1617 last_path.push(CssPathSelector::GeneralSibling);
1618 }
1619 Token::PseudoClass { selector, value } | Token::DoublePseudoClass { selector, value } => {
1620 match pseudo_selector_from_str(selector, value) {
1621 Ok(ps) => last_path.push(CssPathSelector::PseudoSelector(ps)),
1622 Err(e) => {
1623 warn_and_continue!(CssParseWarnMsgInner::SkippedRule {
1624 selector: Some(selector),
1625 error: e.into(),
1626 });
1627 }
1628 }
1629 }
1630 Token::AttributeSelector(attr) => {
1631 if let Some(sel) = parse_attribute_selector(attr) { last_path.push(CssPathSelector::Attribute(sel)) } else { warn_and_continue!(CssParseWarnMsgInner::MalformedStructure {
1632 message: "Malformed attribute selector, rule skipped",
1633 }) }
1634 }
1635 Token::Declaration(key, val) => {
1636 current_declarations.insert(
1637 key,
1638 (val, ErrorLocationRange { start: last_error_location, end: get_error_location(tokenizer) }),
1639 );
1640 }
1641 Token::EndOfStream => {
1642 if block_nesting != 0 {
1643 warnings.push(CssParseWarnMsg {
1644 warning: CssParseWarnMsgInner::MalformedStructure {
1645 message: "Unclosed blocks at end of file",
1646 },
1647 location: ErrorLocationRange { start: last_error_location, end: get_error_location(tokenizer) },
1648 });
1649 }
1650 break;
1651 }
1652 _ => { }
1653 }
1654
1655 last_error_location = get_error_location(tokenizer);
1656 }
1657
1658 let (stylesheet, mut block_warnings) = css_blocks_to_stylesheet(css_blocks, css_string);
1660 warnings.append(&mut block_warnings);
1661
1662 (stylesheet, warnings)
1663}
1664
1665fn css_blocks_to_stylesheet<'a>(
1666 css_blocks: Vec<UnparsedCssRuleBlock<'a>>,
1667 css_string: &'a str,
1668) -> (Vec<CssRuleBlock>, Vec<CssParseWarnMsg<'a>>) {
1669 let css_key_map = crate::props::property::get_css_key_map();
1670 let mut warnings = Vec::new();
1671 let mut parsed_css_blocks = Vec::new();
1672
1673 for unparsed_css_block in css_blocks {
1674 let mut declarations = Vec::<CssDeclaration>::new();
1675
1676 for (unparsed_css_key, (unparsed_css_value, location)) in &unparsed_css_block.declarations {
1677 match parse_declaration_resilient(
1678 unparsed_css_key,
1679 unparsed_css_value,
1680 *location,
1681 &css_key_map,
1682 ) {
1683 Ok(decls) => declarations.extend(decls),
1684 Err(e) => {
1685 warnings.push(CssParseWarnMsg {
1686 warning: CssParseWarnMsgInner::SkippedDeclaration {
1687 key: unparsed_css_key,
1688 value: unparsed_css_value,
1689 error: e,
1690 },
1691 location: *location,
1692 });
1693 }
1694 }
1695 }
1696
1697 parsed_css_blocks.push(CssRuleBlock {
1698 path: unparsed_css_block.path,
1699 declarations: declarations.into(),
1700 conditions: unparsed_css_block.conditions.into(),
1701 priority: crate::css::rule_priority::AUTHOR,
1702 });
1703 }
1704
1705 (parsed_css_blocks, warnings)
1706}
1707
1708fn parse_declaration_resilient<'a>(
1709 unparsed_css_key: &'a str,
1710 unparsed_css_value: &'a str,
1711 location: ErrorLocationRange,
1712 css_key_map: &CssKeyMap,
1713) -> Result<Vec<CssDeclaration>, CssParseErrorInner<'a>> {
1714 let mut declarations = Vec::new();
1715
1716 if let Some(combined_key) = CombinedCssPropertyType::from_str(unparsed_css_key, css_key_map) {
1717 if check_if_value_is_css_var(unparsed_css_value).is_some() {
1718 return Err(CssParseErrorInner::VarOnShorthandProperty {
1719 key: combined_key,
1720 value: unparsed_css_value,
1721 });
1722 }
1723
1724 match parse_combined_css_property(combined_key, unparsed_css_value) {
1726 Ok(parsed_props) => {
1727 declarations.extend(parsed_props.into_iter().map(CssDeclaration::Static));
1728 }
1729 Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
1730 }
1731 } else if let Some(normal_key) = CssPropertyType::from_str(unparsed_css_key, css_key_map) {
1732 if let Some(css_var) = check_if_value_is_css_var(unparsed_css_value) {
1733 let (css_var_id, css_var_default) = css_var?;
1734 match parse_css_property(normal_key, css_var_default) {
1735 Ok(parsed_default) => {
1736 declarations.push(CssDeclaration::Dynamic(DynamicCssProperty {
1737 dynamic_id: css_var_id.to_string().into(),
1738 default_value: parsed_default,
1739 }));
1740 }
1741 Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
1742 }
1743 } else {
1744 match parse_css_property(normal_key, unparsed_css_value) {
1745 Ok(parsed_value) => {
1746 declarations.push(CssDeclaration::Static(parsed_value));
1747 }
1748 Err(e) => return Err(CssParseErrorInner::DynamicCssParseError(e.into())),
1749 }
1750 }
1751 } else {
1752 return Err(CssParseErrorInner::UnknownPropertyKey(
1753 unparsed_css_key,
1754 unparsed_css_value,
1755 ));
1756 }
1757
1758 Ok(declarations)
1759}
1760
1761pub fn parse_css_declaration<'a>(
1770 unparsed_css_key: &'a str,
1771 unparsed_css_value: &'a str,
1772 location: ErrorLocationRange,
1773 css_key_map: &CssKeyMap,
1774 warnings: &mut Vec<CssParseWarnMsg<'a>>,
1775 declarations: &mut Vec<CssDeclaration>,
1776) -> Result<(), CssParseErrorInner<'a>> {
1777 match parse_declaration_resilient(unparsed_css_key, unparsed_css_value, location, css_key_map) {
1778 Ok(mut decls) => {
1779 declarations.append(&mut decls);
1780 Ok(())
1781 }
1782 Err(e) => {
1783 if let CssParseErrorInner::UnknownPropertyKey(key, val) = &e {
1784 warnings.push(CssParseWarnMsg {
1785 warning: CssParseWarnMsgInner::UnsupportedKeyValuePair { key, value: val },
1786 location,
1787 });
1788 Ok(()) } else {
1790 Err(e) }
1792 }
1793 }
1794}
1795
1796fn check_if_value_is_css_var(
1797 unparsed_css_value: &str,
1798) -> Option<Result<(&str, &str), CssParseErrorInner<'_>>> {
1799 const DEFAULT_VARIABLE_DEFAULT: &str = "none";
1800
1801 let (_, brace_contents) = parse_parentheses(unparsed_css_value, &["var"]).ok()?;
1802
1803 Some(match parse_css_variable_brace_contents(brace_contents) {
1805 Some((variable_id, default_value)) => Ok((
1806 variable_id,
1807 default_value.unwrap_or(DEFAULT_VARIABLE_DEFAULT),
1808 )),
1809 None => Err(DynamicCssParseError::InvalidBraceContents(brace_contents).into()),
1810 })
1811}
1812
1813fn parse_css_variable_brace_contents(input: &str) -> Option<(&str, Option<&str>)> {
1820 let input = input.trim();
1821
1822 let mut split_comma_iter = input.splitn(2, ',');
1823 let var_name = split_comma_iter.next()?;
1824 let var_name = var_name.trim();
1825
1826 if !var_name.starts_with("--") {
1827 return None; }
1829
1830 Some((&var_name[2..], split_comma_iter.next()))
1831}
1832
1833#[cfg(test)]
1834#[allow(
1835 clippy::all,
1836 clippy::pedantic,
1837 clippy::nursery,
1838 unused_qualifications,
1839 single_use_lifetimes
1840)]
1841mod autotest_generated {
1842 use super::*;
1843 use crate::css::CssNthChildPattern;
1844
1845 fn catch<R>(f: impl FnOnce() -> R) -> Result<R, String> {
1854 std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_err(|e| {
1855 e.downcast_ref::<String>().cloned().unwrap_or_else(|| {
1856 e.downcast_ref::<&str>()
1857 .map_or_else(|| "<non-string panic payload>".to_string(), |s| (*s).to_string())
1858 })
1859 })
1860 }
1861
1862 fn key_map() -> CssKeyMap {
1863 crate::props::property::get_css_key_map()
1864 }
1865
1866 fn loc(start: usize, end: usize) -> ErrorLocationRange {
1867 ErrorLocationRange {
1868 start: ErrorLocation { original_pos: start },
1869 end: ErrorLocation { original_pos: end },
1870 }
1871 }
1872
1873 const HOSTILE: &[&str] = &[
1875 "",
1876 " ",
1877 " \t\n\r ",
1878 "\0",
1879 "\u{1F600}",
1880 "e\u{301}\u{301}\u{301}",
1881 "-0",
1882 "0",
1883 "NaN",
1884 "inf",
1885 "-inf",
1886 "9223372036854775807",
1887 "-9223372036854775808",
1888 "18446744073709551616",
1889 "1e309",
1890 ";",
1891 "{}",
1892 "()",
1893 "((((",
1894 "))))",
1895 "\"",
1896 "'",
1897 "\\",
1898 "//",
1899 "/*",
1900 "valid;garbage",
1901 " valid ",
1902 "a=b=c",
1903 ":::",
1904 "--",
1905 ];
1906
1907 #[test]
1914 fn pseudo_selector_from_str_valid_minimal() {
1915 assert_eq!(
1916 pseudo_selector_from_str("hover", None),
1917 Ok(CssPathPseudoSelector::Hover)
1918 );
1919 assert_eq!(
1920 pseudo_selector_from_str("first", None),
1921 Ok(CssPathPseudoSelector::First)
1922 );
1923 assert_eq!(
1924 pseudo_selector_from_str("last", None),
1925 Ok(CssPathPseudoSelector::Last)
1926 );
1927 assert_eq!(
1928 pseudo_selector_from_str("active", None),
1929 Ok(CssPathPseudoSelector::Active)
1930 );
1931 assert_eq!(
1932 pseudo_selector_from_str("focus", None),
1933 Ok(CssPathPseudoSelector::Focus)
1934 );
1935 assert_eq!(
1936 pseudo_selector_from_str("dragging", None),
1937 Ok(CssPathPseudoSelector::Dragging)
1938 );
1939 assert_eq!(
1940 pseudo_selector_from_str("drag-over", None),
1941 Ok(CssPathPseudoSelector::DragOver)
1942 );
1943 }
1944
1945 #[test]
1946 fn pseudo_selector_from_str_nth_child_needs_a_value() {
1947 assert_eq!(
1948 pseudo_selector_from_str("nth-child", None),
1949 Err(CssPseudoSelectorParseError::EmptyNthChild)
1950 );
1951 assert_eq!(
1952 pseudo_selector_from_str("nth-child", Some("2")),
1953 Ok(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(2)))
1954 );
1955 }
1956
1957 #[test]
1958 fn pseudo_selector_from_str_lang_strips_quotes() {
1959 for v in ["de-DE", "\"de-DE\"", "'de-DE'", " \"de-DE\" "] {
1961 assert_eq!(
1962 pseudo_selector_from_str("lang", Some(v)),
1963 Ok(CssPathPseudoSelector::Lang(AzString::from("de-DE".to_string()))),
1964 "lang value {v:?} did not normalise to de-DE"
1965 );
1966 }
1967 assert!(pseudo_selector_from_str("lang", None).is_err());
1969 }
1970
1971 #[test]
1972 fn pseudo_selector_from_str_empty_and_whitespace_are_rejected() {
1973 assert!(pseudo_selector_from_str("", None).is_err());
1974 assert!(pseudo_selector_from_str(" ", None).is_err());
1975 assert!(pseudo_selector_from_str("\t\n", None).is_err());
1976 assert!(pseudo_selector_from_str(" hover ", None).is_err());
1978 }
1979
1980 #[test]
1981 fn pseudo_selector_from_str_garbage_and_unicode_never_panic() {
1982 for s in HOSTILE {
1983 for v in [None, Some(*s), Some("2"), Some("\u{1F600}")] {
1984 let r = catch(|| pseudo_selector_from_str(s, v));
1985 assert!(
1986 r.is_ok(),
1987 "pseudo_selector_from_str({s:?}, {v:?}) panicked: {}",
1988 r.unwrap_err()
1989 );
1990 assert!(
1992 pseudo_selector_from_str(s, v).is_err(),
1993 "pseudo_selector_from_str({s:?}, {v:?}) unexpectedly succeeded"
1994 );
1995 }
1996 }
1997 }
1998
1999 #[test]
2000 fn pseudo_selector_from_str_extremely_long_input_terminates() {
2001 let long = "z".repeat(200_000);
2002 assert!(pseudo_selector_from_str(&long, None).is_err());
2003 assert_eq!(
2005 pseudo_selector_from_str("hover", Some(&long)),
2006 Ok(CssPathPseudoSelector::Hover)
2007 );
2008 assert!(pseudo_selector_from_str("nth-child", Some(&long)).is_err());
2010 }
2011
2012 #[test]
2013 fn pseudo_selector_from_str_deeply_nested_value_does_not_stack_overflow() {
2014 let nested = "(".repeat(10_000);
2015 let r = catch(|| pseudo_selector_from_str("nth-child", Some(&nested)).is_err());
2016 assert_eq!(r, Ok(true), "deeply nested nth-child value was not rejected safely");
2017 }
2018
2019 #[test]
2022 fn parse_attribute_selector_valid_minimal() {
2023 let sel = parse_attribute_selector("href").expect("bare attribute name must parse");
2024 assert_eq!(sel.name.as_str(), "href");
2025 assert_eq!(sel.op, AttributeMatchOp::Exists);
2026 assert_eq!(sel.value.clone().into_option(), None);
2027 }
2028
2029 #[test]
2030 fn parse_attribute_selector_all_operators() {
2031 let cases: [(&str, AttributeMatchOp); 6] = [
2032 ("a=b", AttributeMatchOp::Eq),
2033 ("a~=b", AttributeMatchOp::Includes),
2034 ("a|=b", AttributeMatchOp::DashMatch),
2035 ("a^=b", AttributeMatchOp::Prefix),
2036 ("a$=b", AttributeMatchOp::Suffix),
2037 ("a*=b", AttributeMatchOp::Substring),
2038 ];
2039 for (input, expected_op) in cases {
2040 let sel = parse_attribute_selector(input)
2041 .unwrap_or_else(|| panic!("{input:?} should parse"));
2042 assert_eq!(sel.name.as_str(), "a", "wrong name for {input:?}");
2043 assert_eq!(sel.op, expected_op, "wrong op for {input:?}");
2044 assert_eq!(
2045 sel.value.clone().into_option().map(|v| v.as_str().to_string()),
2046 Some("b".to_string()),
2047 "wrong value for {input:?}"
2048 );
2049 }
2050 }
2051
2052 #[test]
2053 fn parse_attribute_selector_quotes_are_stripped_and_unbalanced_rejected() {
2054 for input in ["a=\"b\"", "a='b'", "a=b", " a = \"b\" "] {
2055 let sel = parse_attribute_selector(input)
2056 .unwrap_or_else(|| panic!("{input:?} should parse"));
2057 assert_eq!(
2058 sel.value.clone().into_option().map(|v| v.as_str().to_string()),
2059 Some("b".to_string()),
2060 "quotes not stripped for {input:?}"
2061 );
2062 }
2063 for input in ["a=\"b", "a=b\"", "a='b", "a=b'", "a=\"b'", "a=\"", "a='"] {
2065 assert!(
2066 parse_attribute_selector(input).is_none(),
2067 "unbalanced quote {input:?} should be rejected"
2068 );
2069 }
2070 }
2071
2072 #[test]
2073 fn parse_attribute_selector_empty_and_malformed_are_rejected() {
2074 for input in ["", " ", "\t\n", "=", "=b", " =b", "\"a\"", "'a'"] {
2075 assert!(
2076 parse_attribute_selector(input).is_none(),
2077 "{input:?} should be rejected (empty/quoted name)"
2078 );
2079 }
2080 assert!(parse_attribute_selector("a b").is_none());
2082 assert!(parse_attribute_selector("a b=c").is_none());
2083 }
2084
2085 #[test]
2086 fn parse_attribute_selector_unicode_name_is_accepted_and_does_not_panic() {
2087 let sel = parse_attribute_selector("data-\u{1F600}")
2088 .expect("a non-ASCII attribute name has no whitespace/quotes, so it is accepted");
2089 assert_eq!(sel.name.as_str(), "data-\u{1F600}");
2090
2091 let sel = parse_attribute_selector("lang=\"\u{4E2D}\u{6587}\"").expect("unicode value");
2093 assert_eq!(
2094 sel.value.clone().into_option().map(|v| v.as_str().to_string()),
2095 Some("\u{4E2D}\u{6587}".to_string())
2096 );
2097 }
2098
2099 #[test]
2103 fn parse_attribute_selector_result_invariants_hold_for_hostile_input() {
2104 let long = format!("a={}", "x".repeat(100_000));
2105 let nested = format!("a={}", "[".repeat(10_000));
2106 let mut inputs: Vec<&str> = HOSTILE.to_vec();
2107 inputs.push(&long);
2108 inputs.push(&nested);
2109 inputs.push("title=\"a~=b\"");
2110 inputs.push("a=b=c");
2111 inputs.push("[[[[]]]]");
2112
2113 for input in inputs {
2114 let parsed = match catch(|| parse_attribute_selector(input)) {
2115 Ok(p) => p,
2116 Err(msg) => panic!("parse_attribute_selector({input:?}) panicked: {msg}"),
2117 };
2118 if let Some(sel) = parsed {
2119 let name = sel.name.as_str();
2120 assert!(!name.is_empty(), "empty name accepted for {input:?}");
2121 assert!(
2122 !name.chars().any(|c| c.is_whitespace() || c == '"' || c == '\''),
2123 "name {name:?} contains whitespace/quotes for input {input:?}"
2124 );
2125 }
2126 }
2127 }
2128
2129 #[test]
2132 fn strip_attribute_quotes_balanced_unquoted_and_unbalanced() {
2133 assert_eq!(strip_attribute_quotes("\"abc\""), Some("abc"));
2135 assert_eq!(strip_attribute_quotes("'abc'"), Some("abc"));
2136 assert_eq!(strip_attribute_quotes("\"\""), Some(""));
2137 assert_eq!(strip_attribute_quotes("''"), Some(""));
2138 assert_eq!(strip_attribute_quotes("abc"), Some("abc"));
2140 assert_eq!(strip_attribute_quotes(""), Some(""));
2141 assert_eq!(strip_attribute_quotes("a"), Some("a"));
2142 assert_eq!(strip_attribute_quotes("\"abc"), None);
2144 assert_eq!(strip_attribute_quotes("abc\""), None);
2145 assert_eq!(strip_attribute_quotes("'abc"), None);
2146 assert_eq!(strip_attribute_quotes("abc'"), None);
2147 assert_eq!(strip_attribute_quotes("\"abc'"), None);
2148 assert_eq!(strip_attribute_quotes("\""), None);
2149 assert_eq!(strip_attribute_quotes("'"), None);
2150 }
2151
2152 #[test]
2157 fn strip_attribute_quotes_multibyte_boundaries_never_panic() {
2158 let cases = [
2159 "\u{1F600}",
2160 "\"\u{1F600}\"",
2161 "'\u{4E2D}\u{6587}'",
2162 "\u{4E2D}\u{6587}",
2163 "\"\u{301}\"",
2164 "\u{301}",
2165 ];
2166 for s in cases {
2167 let r = catch(|| strip_attribute_quotes(s));
2168 assert!(r.is_ok(), "strip_attribute_quotes({s:?}) panicked: {}", r.unwrap_err());
2169 }
2170 assert_eq!(strip_attribute_quotes("\"\u{1F600}\""), Some("\u{1F600}"));
2171 assert_eq!(strip_attribute_quotes("\u{1F600}"), Some("\u{1F600}"));
2172 }
2173
2174 #[test]
2175 fn strip_attribute_quotes_extremely_long_input_terminates() {
2176 let long = "x".repeat(500_000);
2177 assert_eq!(strip_attribute_quotes(&long), Some(long.as_str()));
2178 let quoted = format!("\"{long}\"");
2179 assert_eq!(strip_attribute_quotes("ed), Some(long.as_str()));
2180 }
2181
2182 #[test]
2185 fn parse_nth_child_selector_valid_minimal() {
2186 assert_eq!(parse_nth_child_selector("2"), Ok(CssNthChildSelector::Number(2)));
2187 assert_eq!(parse_nth_child_selector("0"), Ok(CssNthChildSelector::Number(0)));
2188 assert_eq!(parse_nth_child_selector("even"), Ok(CssNthChildSelector::Even));
2189 assert_eq!(parse_nth_child_selector("odd"), Ok(CssNthChildSelector::Odd));
2190 assert_eq!(parse_nth_child_selector(" 7 "), Ok(CssNthChildSelector::Number(7)));
2191 assert_eq!(
2192 parse_nth_child_selector("2n+3"),
2193 Ok(CssNthChildSelector::Pattern(CssNthChildPattern {
2194 pattern_repeat: 2,
2195 offset: 3
2196 }))
2197 );
2198 assert_eq!(
2199 parse_nth_child_selector("2n"),
2200 Ok(CssNthChildSelector::Pattern(CssNthChildPattern {
2201 pattern_repeat: 2,
2202 offset: 0
2203 }))
2204 );
2205 }
2206
2207 #[test]
2208 fn parse_nth_child_selector_empty_is_empty_nth_child_error() {
2209 assert_eq!(
2210 parse_nth_child_selector(""),
2211 Err(CssPseudoSelectorParseError::EmptyNthChild)
2212 );
2213 assert_eq!(
2214 parse_nth_child_selector(" \t\n "),
2215 Err(CssPseudoSelectorParseError::EmptyNthChild)
2216 );
2217 assert_eq!(
2218 parse_nth_child_pattern(""),
2219 Err(CssPseudoSelectorParseError::EmptyNthChild)
2220 );
2221 }
2222
2223 #[test]
2226 fn parse_nth_child_selector_numeric_limits_saturate_into_errors() {
2227 assert_eq!(
2228 parse_nth_child_selector("4294967295"),
2229 Ok(CssNthChildSelector::Number(u32::MAX))
2230 );
2231 for overflow in [
2232 "4294967296",
2233 "18446744073709551616",
2234 "99999999999999999999999999",
2235 "-1",
2236 "-0",
2237 ] {
2238 assert!(
2239 parse_nth_child_selector(overflow).is_err(),
2240 "{overflow:?} must not parse as an nth-child index"
2241 );
2242 }
2243 let huge = "9".repeat(100_000);
2245 assert!(parse_nth_child_selector(&huge).is_err());
2246 let huge_repeat = format!("{}n+1", "9".repeat(100_000));
2247 assert!(parse_nth_child_pattern(&huge_repeat).is_err());
2248 }
2249
2250 #[test]
2251 fn parse_nth_child_selector_float_and_non_finite_strings_are_rejected() {
2252 for v in ["NaN", "inf", "-inf", "1.5", "1e5", "0x2", "+2", " 2 n "] {
2253 let r = catch(|| parse_nth_child_selector(v));
2254 assert!(r.is_ok(), "parse_nth_child_selector({v:?}) panicked: {}", r.unwrap_err());
2255 }
2256 assert!(parse_nth_child_selector("NaN").is_err());
2257 assert!(parse_nth_child_selector("inf").is_err());
2258 assert!(parse_nth_child_selector("1.5").is_err());
2259 }
2260
2261 #[test]
2262 fn parse_nth_child_pattern_malformed_offsets_are_rejected() {
2263 assert_eq!(
2265 parse_nth_child_pattern("2n+"),
2266 Err(CssPseudoSelectorParseError::InvalidNthChildPattern("2n+"))
2267 );
2268 assert!(parse_nth_child_pattern("2n+ ").is_err());
2269 assert!(parse_nth_child_pattern("2n+x").is_err());
2270 assert!(parse_nth_child_pattern("xn+1").is_err());
2271 for s in HOSTILE {
2274 let r = catch(|| parse_nth_child_pattern(s));
2275 assert!(r.is_ok(), "parse_nth_child_pattern({s:?}) panicked: {}", r.unwrap_err());
2276 }
2277 }
2278
2279 #[test]
2280 fn parse_nth_child_selector_unicode_never_panics() {
2281 for v in ["\u{1F600}", "\u{FF12}", "2\u{301}", "e\u{301}ven", "\u{4E2D}n+\u{6587}"] {
2282 let r = catch(|| parse_nth_child_selector(v));
2283 assert!(r.is_ok(), "parse_nth_child_selector({v:?}) panicked: {}", r.unwrap_err());
2284 assert!(
2285 parse_nth_child_selector(v).is_err(),
2286 "{v:?} is not a valid nth-child value"
2287 );
2288 }
2289 }
2290
2291 #[test]
2294 fn parse_css_path_valid_minimal() {
2295 assert_eq!(
2297 parse_css_path("* div #my_id > .class:nth-child(2)"),
2298 Ok(CssPath {
2299 selectors: vec![
2300 CssPathSelector::Global,
2301 CssPathSelector::Type(NodeTypeTag::from_str("div").unwrap()),
2302 CssPathSelector::Children,
2303 CssPathSelector::Id("my_id".to_string().into()),
2304 CssPathSelector::DirectChildren,
2305 CssPathSelector::Class("class".to_string().into()),
2306 CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
2307 CssNthChildSelector::Number(2)
2308 )),
2309 ]
2310 .into()
2311 })
2312 );
2313 assert_eq!(
2314 parse_css_path("div"),
2315 Ok(CssPath {
2316 selectors: vec![CssPathSelector::Type(NodeTypeTag::from_str("div").unwrap())]
2317 .into()
2318 })
2319 );
2320 }
2321
2322 #[test]
2323 fn parse_css_path_empty_and_whitespace_are_empty_path_errors() {
2324 assert_eq!(parse_css_path(""), Err(CssPathParseError::EmptyPath));
2325 assert_eq!(parse_css_path(" "), Err(CssPathParseError::EmptyPath));
2326 assert_eq!(parse_css_path("\t\r\n "), Err(CssPathParseError::EmptyPath));
2327 assert_eq!(
2329 parse_css_path("definitelynotatag"),
2330 Err(CssPathParseError::EmptyPath)
2331 );
2332 }
2333
2334 #[test]
2335 fn parse_css_path_garbage_and_unicode_never_panic() {
2336 let long = "div ".repeat(50_000);
2337 let brackets = "[".repeat(10_000);
2338 let braces = "{".repeat(10_000);
2339 let mut inputs: Vec<&str> = HOSTILE.to_vec();
2340 inputs.push(&long);
2341 inputs.push(&brackets);
2342 inputs.push(&braces);
2343 inputs.push("div;garbage");
2344 inputs.push("div }");
2345 inputs.push(":::::");
2346 inputs.push("\u{1F600} > \u{4E2D}\u{6587}");
2347
2348 for input in inputs {
2349 let r = catch(|| parse_css_path(input));
2350 assert!(r.is_ok(), "parse_css_path({:.40?}) panicked: {}", input, r.unwrap_err());
2351 }
2352 }
2353
2354 #[test]
2355 fn parse_css_path_rejects_block_tokens() {
2356 for input in ["div { }", "div {", "}"] {
2358 assert!(
2359 parse_css_path(input).is_err(),
2360 "{input:?} contains block tokens and must not parse as a path"
2361 );
2362 }
2363 }
2364
2365 #[test]
2366 fn parse_css_path_unknown_pseudo_selector_is_an_error() {
2367 assert!(parse_css_path("div:definitelynotapseudo").is_err());
2368 assert!(parse_css_path(".x:nth-child(notanumber)").is_err());
2369 }
2370
2371 #[test]
2379 fn parse_css_path_unknown_type_tag_is_not_silently_dropped() {
2380 let parsed = parse_css_path("div definitelynotatag");
2381 if let Ok(path) = &parsed {
2382 let selectors = path.selectors.as_slice();
2383 assert!(
2384 !matches!(
2385 selectors.last(),
2386 Some(
2387 CssPathSelector::Children
2388 | CssPathSelector::DirectChildren
2389 | CssPathSelector::AdjacentSibling
2390 | CssPathSelector::GeneralSibling
2391 )
2392 ),
2393 "BUG: the unknown type tag was dropped, leaving a dangling combinator; \
2394 `div definitelynotatag` now matches every descendant of div. \
2395 selectors = {selectors:?}"
2396 );
2397 }
2398 }
2399
2400 #[test]
2403 fn parse_media_conditions_valid_minimal() {
2404 assert_eq!(
2405 parse_media_conditions("screen"),
2406 vec![DynamicSelector::Media(MediaType::Screen)]
2407 );
2408 assert_eq!(
2409 parse_media_conditions("PRINT"),
2410 vec![DynamicSelector::Media(MediaType::Print)]
2411 );
2412 assert_eq!(
2413 parse_media_conditions("all"),
2414 vec![DynamicSelector::Media(MediaType::All)]
2415 );
2416 }
2417
2418 #[test]
2419 fn parse_media_conditions_parenthesised_and_compound() {
2420 let conds = parse_media_conditions("(min-width: 800px)");
2421 assert_eq!(conds.len(), 1);
2422 match &conds[0] {
2423 DynamicSelector::ViewportWidth(r) => {
2424 assert_eq!(r.min(), Some(800.0));
2425 assert_eq!(r.max(), None);
2426 }
2427 other => panic!("expected ViewportWidth, got {other:?}"),
2428 }
2429
2430 let conds = parse_media_conditions("screen and (max-width: 600px)");
2431 assert_eq!(conds.len(), 2, "compound query should yield both conditions");
2432 assert_eq!(conds[0], DynamicSelector::Media(MediaType::Screen));
2433 match &conds[1] {
2434 DynamicSelector::ViewportWidth(r) => {
2435 assert_eq!(r.min(), None);
2436 assert_eq!(r.max(), Some(600.0));
2437 }
2438 other => panic!("expected ViewportWidth, got {other:?}"),
2439 }
2440 }
2441
2442 #[test]
2443 fn parse_media_conditions_empty_and_garbage_yield_no_conditions() {
2444 for input in ["", " ", "((((", "))))", "\u{1F600}", "and", "(", ")", "()"] {
2445 let r = catch(|| parse_media_conditions(input));
2446 match r {
2447 Ok(conds) => assert!(
2448 conds.is_empty(),
2449 "{input:?} should not produce media conditions, got {conds:?}"
2450 ),
2451 Err(msg) => panic!("parse_media_conditions({input:?}) panicked: {msg}"),
2452 }
2453 }
2454 }
2455
2456 #[test]
2457 fn parse_media_conditions_extremely_long_and_deeply_nested_terminate() {
2458 let nested = format!("({})", "(".repeat(10_000));
2459 let r = catch(|| parse_media_conditions(&nested));
2460 assert!(r.is_ok(), "deeply nested media query panicked: {}", r.unwrap_err());
2461
2462 let long = "screen and ".repeat(20_000);
2463 let r = catch(|| parse_media_conditions(&long));
2464 assert!(r.is_ok(), "very long media query panicked: {}", r.unwrap_err());
2465 }
2466
2467 #[test]
2468 fn parse_media_feature_known_features() {
2469 assert_eq!(
2470 parse_media_feature("orientation: portrait"),
2471 Some(DynamicSelector::Orientation(OrientationType::Portrait))
2472 );
2473 assert_eq!(
2474 parse_media_feature("orientation: LANDSCAPE"),
2475 Some(DynamicSelector::Orientation(OrientationType::Landscape))
2476 );
2477 assert_eq!(
2478 parse_media_feature("prefers-color-scheme: dark"),
2479 Some(DynamicSelector::Theme(ThemeCondition::Dark))
2480 );
2481 assert_eq!(
2482 parse_media_feature("prefers-reduced-motion: reduce"),
2483 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
2484 );
2485 assert_eq!(
2486 parse_media_feature("prefers-contrast: more"),
2487 Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
2488 );
2489 assert!(parse_media_feature("MIN-WIDTH: 800px").is_some());
2491 }
2492
2493 #[test]
2494 fn parse_media_feature_malformed_returns_none() {
2495 for input in [
2496 "",
2497 " ",
2498 "nocolon",
2499 "min-width:",
2500 "min-width: ",
2501 "min-width: abc",
2502 ": 800px",
2503 "unknown-feature: 800px",
2504 "orientation: sideways",
2505 "\u{1F600}: \u{1F600}",
2506 ] {
2507 let r = catch(|| parse_media_feature(input));
2508 match r {
2509 Ok(v) => assert!(v.is_none(), "{input:?} should be rejected, got {v:?}"),
2510 Err(msg) => panic!("parse_media_feature({input:?}) panicked: {msg}"),
2511 }
2512 }
2513 }
2514
2515 #[test]
2521 fn parse_media_feature_nan_width_does_not_erase_the_constraint() {
2522 for feature in ["min-width: NaN", "min-width: NaNpx", "max-width: nan"] {
2523 match parse_media_feature(feature) {
2524 None => {} Some(DynamicSelector::ViewportWidth(r)) => {
2526 assert!(
2527 r.min().is_some() || r.max().is_some(),
2528 "BUG: {feature:?} parsed into a ViewportWidth with no bounds at all \
2529 (the NaN collided with MinMaxRange's `absent` sentinel), so the \
2530 media query silently matches every viewport"
2531 );
2532 }
2533 Some(other) => panic!("unexpected selector for {feature:?}: {other:?}"),
2534 }
2535 }
2536 }
2537
2538 #[test]
2541 fn parse_px_value_valid_minimal() {
2542 assert_eq!(parse_px_value("800px"), Some(800.0));
2543 assert_eq!(parse_px_value("800"), Some(800.0));
2544 assert_eq!(parse_px_value(" 800px "), Some(800.0));
2545 assert_eq!(parse_px_value("0"), Some(0.0));
2546 assert_eq!(parse_px_value("1.5px"), Some(1.5));
2547 assert_eq!(parse_px_value("-10px"), Some(-10.0));
2548 }
2549
2550 #[test]
2551 fn parse_px_value_malformed_returns_none() {
2552 for input in ["", " ", "px", "abc", "8 0 0", "800pxx", "\u{1F600}", "800%", "--"] {
2553 let r = catch(|| parse_px_value(input));
2554 match r {
2555 Ok(v) => assert!(v.is_none(), "{input:?} should be rejected, got {v:?}"),
2556 Err(msg) => panic!("parse_px_value({input:?}) panicked: {msg}"),
2557 }
2558 }
2559 }
2560
2561 #[test]
2564 fn parse_px_value_overflow_and_underflow_saturate_without_panicking() {
2565 assert_eq!(parse_px_value("-0"), Some(-0.0));
2566 assert_eq!(parse_px_value("1e-50"), Some(0.0));
2567 assert_eq!(parse_px_value("3.5e38px"), Some(3.5e38));
2568
2569 let overflowed = parse_px_value("1e39px").expect("f32 overflow still parses");
2570 assert!(overflowed.is_infinite(), "1e39 should saturate to infinity");
2571
2572 let huge_digits = "9".repeat(100_000);
2573 let r = catch(|| parse_px_value(&huge_digits));
2574 assert!(r.is_ok(), "a 100k-digit number panicked: {}", r.unwrap_err());
2575 }
2576
2577 #[test]
2583 fn parse_px_value_rejects_non_finite_values() {
2584 for input in ["NaN", "nan", "inf", "-inf", "infinity", "NaNpx", "infpx", "-infpx"] {
2585 if let Some(px) = parse_px_value(input) {
2586 assert!(
2587 px.is_finite(),
2588 "BUG: parse_px_value({input:?}) returned the non-finite value {px}; \
2589 a non-finite length is not valid CSS and collides with MinMaxRange's \
2590 NaN `absent` sentinel"
2591 );
2592 }
2593 }
2594 }
2595
2596 #[test]
2599 fn parse_ratio_value_valid_minimal() {
2600 let r = parse_ratio_value("16/9").expect("16/9 should parse");
2601 assert!((r - (16.0 / 9.0)).abs() < 1e-6, "16/9 parsed as {r}");
2602 let r = parse_ratio_value("1.777").expect("bare float should parse");
2603 assert!((r - 1.777).abs() < 1e-6);
2604 let r = parse_ratio_value(" 16 / 9 ").expect("whitespace should be trimmed");
2605 assert!((r - (16.0 / 9.0)).abs() < 1e-6);
2606 }
2607
2608 #[test]
2609 fn parse_ratio_value_division_by_zero_is_rejected() {
2610 assert_eq!(parse_ratio_value("1/0"), None);
2612 assert_eq!(parse_ratio_value("1/-0"), None);
2613 assert_eq!(parse_ratio_value("0/0"), None);
2614 assert_eq!(parse_ratio_value("16/0.0"), None);
2615 }
2616
2617 #[test]
2618 fn parse_ratio_value_malformed_returns_none() {
2619 for input in ["", " ", "/", "16/", "/9", "a/b", "1/2/3", "\u{1F600}", "16:9"] {
2620 let r = catch(|| parse_ratio_value(input));
2621 match r {
2622 Ok(v) => assert!(v.is_none(), "{input:?} should be rejected, got {v:?}"),
2623 Err(msg) => panic!("parse_ratio_value({input:?}) panicked: {msg}"),
2624 }
2625 }
2626 }
2627
2628 #[test]
2633 fn parse_ratio_value_never_returns_nan() {
2634 for input in ["NaN", "inf/inf", "NaN/1", "1/NaN", "-inf/inf", "inf/-inf"] {
2635 if let Some(r) = parse_ratio_value(input) {
2636 assert!(
2637 !r.is_nan(),
2638 "BUG: parse_ratio_value({input:?}) returned NaN, which MinMaxRange \
2639 reads back as `no bound` -- the aspect-ratio query silently matches \
2640 everything"
2641 );
2642 }
2643 }
2644 }
2645
2646 #[test]
2647 fn parse_ratio_value_extremely_long_input_terminates() {
2648 let huge = format!("{}/{}", "9".repeat(50_000), "9".repeat(50_000));
2649 let r = catch(|| parse_ratio_value(&huge));
2650 assert!(r.is_ok(), "huge ratio panicked: {}", r.unwrap_err());
2651 }
2652
2653 #[test]
2656 fn parse_container_conditions_valid_minimal() {
2657 assert_eq!(
2659 parse_container_conditions("sidebar"),
2660 vec![DynamicSelector::ContainerName(AzString::from(
2661 "sidebar".to_string()
2662 ))]
2663 );
2664
2665 let conds = parse_container_conditions("(min-width: 400px)");
2667 assert_eq!(conds.len(), 1);
2668 match &conds[0] {
2669 DynamicSelector::ContainerWidth(r) => {
2670 assert_eq!(r.min(), Some(400.0));
2671 assert_eq!(r.max(), None);
2672 }
2673 other => panic!("expected ContainerWidth, got {other:?}"),
2674 }
2675
2676 let conds = parse_container_conditions("sidebar (min-width: 400px)");
2678 assert_eq!(conds.len(), 2);
2679 assert_eq!(
2680 conds[0],
2681 DynamicSelector::ContainerName(AzString::from("sidebar".to_string()))
2682 );
2683 assert!(matches!(conds[1], DynamicSelector::ContainerWidth(_)));
2684 }
2685
2686 #[test]
2687 fn parse_container_conditions_empty_and_garbage_never_panic() {
2688 assert!(parse_container_conditions("").is_empty());
2689 assert!(parse_container_conditions(" ").is_empty());
2690
2691 let nested = "(".repeat(10_000);
2692 let long = "a".repeat(200_000);
2693 let mut inputs: Vec<&str> = HOSTILE.to_vec();
2694 inputs.push(&nested);
2695 inputs.push(&long);
2696
2697 for input in inputs {
2698 let r = catch(|| parse_container_conditions(input));
2699 assert!(
2700 r.is_ok(),
2701 "parse_container_conditions({:.40?}) panicked: {}",
2702 input,
2703 r.unwrap_err()
2704 );
2705 }
2706 }
2707
2708 #[test]
2709 fn parse_container_feature_malformed_returns_none() {
2710 for input in ["", " ", "nocolon", "min-width:", "min-width: abc", "unknown: 1px"] {
2711 let r = catch(|| parse_container_feature(input));
2712 match r {
2713 Ok(v) => assert!(v.is_none(), "{input:?} should be rejected, got {v:?}"),
2714 Err(msg) => panic!("parse_container_feature({input:?}) panicked: {msg}"),
2715 }
2716 }
2717 assert!(parse_container_feature("min-height: 400px").is_some());
2718 assert!(parse_container_feature("MAX-WIDTH: 400px").is_some());
2719 }
2720
2721 #[test]
2724 fn parse_theme_condition_valid_minimal() {
2725 for input in ["dark", "(dark)", "DARK", "\"dark\"", "'dark'", "(\"dark\")", " dark "] {
2726 assert_eq!(
2727 parse_theme_condition(input),
2728 Some(DynamicSelector::Theme(ThemeCondition::Dark)),
2729 "theme {input:?} should resolve to Dark"
2730 );
2731 }
2732 assert_eq!(
2733 parse_theme_condition("light"),
2734 Some(DynamicSelector::Theme(ThemeCondition::Light))
2735 );
2736 }
2737
2738 #[test]
2739 fn parse_theme_condition_garbage_returns_none() {
2740 for input in ["", " ", "(", ")", "()", "sepia", "\u{1F600}", "dark light", "\"dark"] {
2741 let r = catch(|| parse_theme_condition(input));
2742 match r {
2743 Ok(v) => assert!(v.is_none(), "theme {input:?} should be rejected, got {v:?}"),
2744 Err(msg) => panic!("parse_theme_condition({input:?}) panicked: {msg}"),
2745 }
2746 }
2747 }
2748
2749 #[test]
2750 fn parse_lang_condition_valid_minimal() {
2751 for input in ["de-DE", "(de-DE)", "(\"de-DE\")", "('de-DE')", " de-DE "] {
2752 assert_eq!(
2753 parse_lang_condition(input),
2754 Some(DynamicSelector::Language(LanguageCondition::Prefix(
2755 AzString::from("de-DE".to_string())
2756 ))),
2757 "lang {input:?} should resolve to Prefix(de-DE)"
2758 );
2759 }
2760 }
2761
2762 #[test]
2763 fn parse_lang_condition_empty_returns_none() {
2764 for input in ["", " ", "()", "( )", "( )"] {
2765 assert_eq!(
2766 parse_lang_condition(input),
2767 None,
2768 "empty lang {input:?} must not produce a condition"
2769 );
2770 }
2771 }
2772
2773 #[test]
2774 fn parse_lang_condition_unicode_and_long_input_never_panic() {
2775 let long = "a".repeat(200_000);
2776 let mut inputs: Vec<&str> = HOSTILE.to_vec();
2777 inputs.push(&long);
2778 for input in inputs {
2779 let r = catch(|| parse_lang_condition(input));
2780 assert!(
2781 r.is_ok(),
2782 "parse_lang_condition({:.40?}) panicked: {}",
2783 input,
2784 r.unwrap_err()
2785 );
2786 }
2787 }
2788
2789 #[test]
2792 fn parse_css_variable_brace_contents_valid_minimal() {
2793 assert_eq!(
2794 parse_css_variable_brace_contents("--main-bg-col"),
2795 Some(("main-bg-col", None))
2796 );
2797 let (name, default) = parse_css_variable_brace_contents("--main-bg-col, blue")
2798 .expect("var with default should parse");
2799 assert_eq!(name, "main-bg-col");
2800 assert_eq!(default.map(str::trim), Some("blue"));
2803 }
2804
2805 #[test]
2806 fn parse_css_variable_brace_contents_rejects_non_variables() {
2807 for input in ["", " ", "main-bg-col", "-main-bg-col", "blue", "\u{1F600}", ","] {
2808 assert_eq!(
2809 parse_css_variable_brace_contents(input),
2810 None,
2811 "{input:?} is not a `--` prefixed CSS variable"
2812 );
2813 }
2814 }
2815
2816 #[test]
2820 fn parse_css_variable_brace_contents_multibyte_name_does_not_split_a_char() {
2821 assert_eq!(
2822 parse_css_variable_brace_contents("--\u{1F600}"),
2823 Some(("\u{1F600}", None))
2824 );
2825 let r = catch(|| parse_css_variable_brace_contents("--"));
2827 assert!(r.is_ok(), "`--` panicked: {}", r.unwrap_err());
2828 }
2829
2830 #[test]
2831 fn check_if_value_is_css_var_recognises_var_syntax() {
2832 assert!(check_if_value_is_css_var("100px").is_none());
2834 assert!(check_if_value_is_css_var("").is_none());
2835 assert!(check_if_value_is_css_var("calc(1px + 2px)").is_none());
2836
2837 match check_if_value_is_css_var("var(--main-bg-color)") {
2839 Some(Ok((id, default))) => {
2840 assert_eq!(id, "main-bg-color");
2841 assert_eq!(default, "none");
2842 }
2843 other => panic!("expected Some(Ok(..)), got {other:?}"),
2844 }
2845
2846 match check_if_value_is_css_var("var(--w, 100px)") {
2848 Some(Ok((id, default))) => {
2849 assert_eq!(id, "w");
2850 assert_eq!(default.trim(), "100px");
2851 }
2852 other => panic!("expected Some(Ok(..)), got {other:?}"),
2853 }
2854
2855 assert!(matches!(
2857 check_if_value_is_css_var("var(nonsense)"),
2858 Some(Err(CssParseErrorInner::DynamicCssParseError(
2859 DynamicCssParseError::InvalidBraceContents(_)
2860 )))
2861 ));
2862 assert!(matches!(
2863 check_if_value_is_css_var("var()"),
2864 Some(Err(CssParseErrorInner::DynamicCssParseError(
2865 DynamicCssParseError::InvalidBraceContents(_)
2866 )))
2867 ));
2868 }
2869
2870 #[test]
2871 fn check_if_value_is_css_var_hostile_input_never_panics() {
2872 let long = format!("var(--{})", "x".repeat(100_000));
2873 let nested = format!("var({})", "(".repeat(10_000));
2874 let mut inputs: Vec<&str> = HOSTILE.to_vec();
2875 inputs.push(&long);
2876 inputs.push(&nested);
2877 inputs.push("var(");
2878 inputs.push("var)");
2879 inputs.push("var((--x))");
2880
2881 for input in inputs {
2882 let r = catch(|| check_if_value_is_css_var(input).is_some());
2883 assert!(
2884 r.is_ok(),
2885 "check_if_value_is_css_var({:.40?}) panicked: {}",
2886 input,
2887 r.unwrap_err()
2888 );
2889 }
2890 }
2891
2892 #[test]
2895 fn parse_css_declaration_valid_minimal() {
2896 let km = key_map();
2897 let mut warnings = Vec::new();
2898 let mut declarations = Vec::new();
2899 let r = parse_css_declaration(
2900 "width",
2901 "100px",
2902 loc(0, 0),
2903 &km,
2904 &mut warnings,
2905 &mut declarations,
2906 );
2907 assert_eq!(r, Ok(()));
2908 assert_eq!(declarations.len(), 1);
2909 assert!(matches!(declarations[0], CssDeclaration::Static(_)));
2910 assert!(warnings.is_empty());
2911 }
2912
2913 #[test]
2914 fn parse_css_declaration_unknown_key_is_downgraded_to_a_warning() {
2915 let km = key_map();
2916 let mut warnings = Vec::new();
2917 let mut declarations = Vec::new();
2918 let r = parse_css_declaration(
2921 "definitely-not-a-property",
2922 "1",
2923 loc(0, 0),
2924 &km,
2925 &mut warnings,
2926 &mut declarations,
2927 );
2928 assert_eq!(r, Ok(()));
2929 assert!(declarations.is_empty());
2930 assert_eq!(warnings.len(), 1);
2931 assert!(matches!(
2932 warnings[0].warning,
2933 CssParseWarnMsgInner::UnsupportedKeyValuePair { .. }
2934 ));
2935 }
2936
2937 #[test]
2938 fn parse_css_declaration_bad_value_is_a_hard_error() {
2939 let km = key_map();
2940 let mut warnings = Vec::new();
2941 let mut declarations = Vec::new();
2942 let r = parse_css_declaration(
2943 "width",
2944 "definitely-not-a-length",
2945 loc(0, 0),
2946 &km,
2947 &mut warnings,
2948 &mut declarations,
2949 );
2950 assert!(r.is_err(), "a known key with an unparseable value must error");
2951 assert!(declarations.is_empty());
2952 }
2953
2954 #[test]
2955 fn parse_declaration_resilient_var_on_shorthand_is_rejected() {
2956 let km = key_map();
2957 let r = parse_declaration_resilient("margin", "var(--m)", loc(0, 0), &km);
2959 assert!(
2960 matches!(r, Err(CssParseErrorInner::VarOnShorthandProperty { .. })),
2961 "expected VarOnShorthandProperty, got {r:?}"
2962 );
2963 }
2964
2965 #[test]
2966 fn parse_declaration_resilient_var_on_normal_property_becomes_dynamic() {
2967 let km = key_map();
2968 let decls = parse_declaration_resilient("width", "var(--w, 100px)", loc(0, 0), &km)
2969 .expect("var() on a non-shorthand property should parse");
2970 assert_eq!(decls.len(), 1);
2971 match &decls[0] {
2972 CssDeclaration::Dynamic(DynamicCssProperty { dynamic_id, .. }) => {
2973 assert_eq!(dynamic_id.as_str(), "w");
2974 }
2975 other => panic!("expected a Dynamic declaration, got {other:?}"),
2976 }
2977 }
2978
2979 #[test]
2980 fn parse_declaration_resilient_hostile_key_value_pairs_never_panic() {
2981 let km = key_map();
2982 let long = "x".repeat(100_000);
2983 let mut inputs: Vec<&str> = HOSTILE.to_vec();
2984 inputs.push(&long);
2985
2986 for key in &inputs {
2987 for value in &inputs {
2988 let r = catch(|| parse_declaration_resilient(key, value, loc(0, 0), &km).is_ok());
2989 assert!(
2990 r.is_ok(),
2991 "parse_declaration_resilient({:.30?}, {:.30?}) panicked: {}",
2992 key,
2993 value,
2994 r.unwrap_err()
2995 );
2996 }
2997 }
2998 }
2999
3000 #[test]
3001 fn parse_declaration_resilient_empty_key_is_an_unknown_property() {
3002 let km = key_map();
3003 assert!(matches!(
3004 parse_declaration_resilient("", "", loc(0, 0), &km),
3005 Err(CssParseErrorInner::UnknownPropertyKey("", ""))
3006 ));
3007 }
3008
3009 #[test]
3014 fn get_line_column_from_error_representative_values() {
3015 let css = "div {\n width: 100px;\n}";
3016 assert_eq!(
3018 ErrorLocation { original_pos: 0 }.get_line_column_from_error(css),
3019 (0, 0)
3020 );
3021 let (line, _col) = ErrorLocation { original_pos: 12 }.get_line_column_from_error(css);
3022 assert_eq!(line, 2, "byte 11 is on the second line");
3023 }
3024
3025 #[test]
3026 fn get_line_column_from_error_empty_css_does_not_panic() {
3027 let r = catch(|| ErrorLocation { original_pos: 0 }.get_line_column_from_error(""));
3028 assert_eq!(r, Ok((0, 0)));
3029 }
3030
3031 #[test]
3034 fn get_line_column_from_error_newline_heavy_input_does_not_underflow() {
3035 let newlines = "\n".repeat(1_000);
3036 let crlf = "\r\n".repeat(1_000);
3037 for css in [newlines.as_str(), crlf.as_str()] {
3038 for pos in [1_usize, 2, 3, 500, css.len()] {
3039 let r = catch(|| ErrorLocation { original_pos: pos }.get_line_column_from_error(css));
3040 assert!(
3041 r.is_ok(),
3042 "get_line_column_from_error(pos={pos}) underflowed/panicked: {}",
3043 r.unwrap_err()
3044 );
3045 }
3046 }
3047 }
3048
3049 #[test]
3055 fn get_line_column_from_error_out_of_range_pos_does_not_panic() {
3056 let css = "div {}";
3057 for pos in [css.len() + 2, 999, usize::MAX] {
3058 if let Err(msg) =
3059 catch(|| ErrorLocation { original_pos: pos }.get_line_column_from_error(css))
3060 {
3061 panic!(
3062 "BUG: get_line_column_from_error panicked for original_pos={pos} on a \
3063 {}-byte string (unchecked `css_string[0..error_location]` slice); it \
3064 should clamp instead: {msg}",
3065 css.len()
3066 );
3067 }
3068 }
3069 }
3070
3071 #[test]
3077 fn get_line_column_from_error_at_end_of_unicode_css_does_not_panic() {
3078 let css = "a\u{1F600}";
3080 assert_eq!(css.len(), 5);
3081 let pos = css.len(); if let Err(msg) =
3083 catch(|| ErrorLocation { original_pos: pos }.get_line_column_from_error(css))
3084 {
3085 panic!(
3086 "BUG: get_line_column_from_error panicked at end-of-stream (original_pos={pos}) \
3087 because the CSS ends with a multi-byte char and `original_pos - 1` splits it: \
3088 {msg}"
3089 );
3090 }
3091 }
3092
3093 #[test]
3098 fn get_error_string_returns_the_trimmed_slice_between_start_and_end() {
3099 let css = "div { width: 100px; }";
3100 let err = CssParseError {
3101 css_string: css,
3102 error: CssParseErrorInner::MalformedCss,
3103 location: loc(6, 18),
3104 };
3105 assert_eq!(err.get_error_string(), "width: 100px");
3106
3107 let err = CssParseError {
3109 css_string: css,
3110 error: CssParseErrorInner::MalformedCss,
3111 location: loc(0, 0),
3112 };
3113 assert_eq!(err.get_error_string(), "");
3114 }
3115
3116 #[test]
3122 fn get_error_string_invalid_location_does_not_panic() {
3123 let cases: [(&str, ErrorLocationRange, &str); 3] = [
3124 ("div", loc(0, 99), "end past the end of the string"),
3125 ("div", loc(2, 1), "reversed range (start > end)"),
3126 ("a\u{1F600}", loc(0, 4), "end inside a multi-byte char"),
3127 ];
3128 for (css, location, why) in cases {
3129 let err = CssParseError {
3130 css_string: css,
3131 error: CssParseErrorInner::MalformedCss,
3132 location,
3133 };
3134 if let Err(msg) = catch(|| err.get_error_string().to_string()) {
3135 panic!(
3136 "BUG: get_error_string panicked on an invalid location ({why}) instead of \
3137 returning an empty/clamped slice: {msg}"
3138 );
3139 }
3140 }
3141 }
3142
3143 #[test]
3146 fn display_of_css_parse_error_is_non_empty_and_well_formed() {
3147 let css = "div { width: 100px; }";
3148 let err = CssParseError {
3149 css_string: css,
3150 error: CssParseErrorInner::MalformedCss,
3151 location: loc(6, 18),
3152 };
3153 let s = format!("{err}");
3154 assert!(!s.is_empty());
3155 assert!(s.contains("start: line"), "missing start location: {s}");
3156 assert!(s.contains("end: line"), "missing end location: {s}");
3157 assert!(s.contains("width: 100px"), "missing offending text: {s}");
3158 assert!(s.contains("Malformed Css"), "missing reason: {s}");
3159 }
3160
3161 #[test]
3162 fn display_of_css_parse_error_on_zero_value_does_not_panic() {
3163 let err = CssParseError {
3164 css_string: "",
3165 error: CssParseErrorInner::UnclosedBlock,
3166 location: ErrorLocationRange::default(),
3167 };
3168 let r = catch(|| format!("{err}"));
3169 match r {
3170 Ok(s) => assert!(!s.is_empty(), "Display produced an empty string"),
3171 Err(msg) => panic!("Display panicked on a zero-valued CssParseError: {msg}"),
3172 }
3173 }
3174
3175 #[test]
3180 fn display_of_css_parse_error_with_unicode_css_does_not_panic() {
3181 let css = "p{}\u{1F600}"; let err = CssParseError {
3183 css_string: css,
3184 error: CssParseErrorInner::MalformedCss,
3185 location: loc(0, css.len()),
3186 };
3187 if let Err(msg) = catch(|| format!("{err}")) {
3188 panic!(
3189 "BUG: Display for CssParseError panicked while formatting an error whose CSS \
3190 ends in a multi-byte char (unchecked slicing in get_line_column_from_error / \
3191 get_error_string): {msg}"
3192 );
3193 }
3194 }
3195
3196 #[test]
3197 fn display_of_error_and_warning_inners_is_never_empty() {
3198 let km = key_map();
3199 let margin = CombinedCssPropertyType::from_str("margin", &km).expect("margin is a shorthand");
3200
3201 let inners: Vec<CssParseErrorInner<'_>> = vec![
3202 CssParseErrorInner::ParseError(CssSyntaxError::UnknownToken(CssSyntaxErrorPos {
3203 row: usize::MAX,
3204 col: usize::MAX,
3205 })),
3206 CssParseErrorInner::UnclosedBlock,
3207 CssParseErrorInner::MalformedCss,
3208 CssParseErrorInner::DynamicCssParseError(DynamicCssParseError::InvalidBraceContents(
3209 "",
3210 )),
3211 CssParseErrorInner::PseudoSelectorParseError(
3212 CssPseudoSelectorParseError::EmptyNthChild,
3213 ),
3214 CssParseErrorInner::NodeTypeTag(NodeTypeTagParseError::Invalid("")),
3215 CssParseErrorInner::UnknownPropertyKey("", ""),
3216 CssParseErrorInner::VarOnShorthandProperty {
3217 key: margin,
3218 value: "",
3219 },
3220 ];
3221
3222 for inner in &inners {
3223 let s = format!("{inner}");
3224 assert!(!s.is_empty(), "empty Display for {inner:?}");
3225 }
3226
3227 let warnings = vec![
3228 CssParseWarnMsgInner::UnsupportedKeyValuePair { key: "", value: "" },
3229 CssParseWarnMsgInner::ParseError(CssParseErrorInner::MalformedCss),
3230 CssParseWarnMsgInner::SkippedRule {
3231 selector: None,
3232 error: CssParseErrorInner::UnclosedBlock,
3233 },
3234 CssParseWarnMsgInner::SkippedDeclaration {
3235 key: "",
3236 value: "",
3237 error: CssParseErrorInner::MalformedCss,
3238 },
3239 CssParseWarnMsgInner::MalformedStructure { message: "" },
3240 ];
3241 for w in &warnings {
3242 assert!(!format!("{w}").is_empty(), "empty Display for {w:?}");
3243 }
3244 }
3245
3246 #[test]
3251 fn css_pseudo_selector_parse_error_round_trips() {
3252 let cases = vec![
3253 CssPseudoSelectorParseError::EmptyNthChild,
3254 CssPseudoSelectorParseError::UnknownSelector("blah", None),
3255 CssPseudoSelectorParseError::UnknownSelector("blah", Some("3")),
3256 CssPseudoSelectorParseError::InvalidNthChildPattern("2x+1"),
3257 CssPseudoSelectorParseError::InvalidNthChild(
3258 "x".parse::<u32>().unwrap_err(),
3259 ),
3260 CssPseudoSelectorParseError::InvalidNthChild(
3261 "99999999999999999999".parse::<u32>().unwrap_err(),
3262 ),
3263 CssPseudoSelectorParseError::UnknownSelector("", Some("")),
3265 ];
3266 for case in &cases {
3267 assert_eq!(
3268 &case.to_contained().to_shared(),
3269 case,
3270 "round-trip changed the value"
3271 );
3272 }
3273 }
3274
3275 #[test]
3276 fn dynamic_css_parse_error_round_trips() {
3277 let simple = DynamicCssParseError::InvalidBraceContents("--x, blue");
3278 assert_eq!(&simple.to_contained().to_shared(), &simple);
3279
3280 let empty = DynamicCssParseError::InvalidBraceContents("");
3281 assert_eq!(&empty.to_contained().to_shared(), &empty);
3282
3283 let km = key_map();
3286 let width = CssPropertyType::from_str("width", &km).expect("width is a property");
3287 let inner = parse_css_property(width, "definitely-not-a-length")
3288 .expect_err("an invalid length must fail to parse");
3289 let wrapped = DynamicCssParseError::UnexpectedValue(inner);
3290 let round_tripped = wrapped.to_contained();
3291 let back = round_tripped.to_shared();
3292 assert!(matches!(back, DynamicCssParseError::UnexpectedValue(_)));
3293 assert_eq!(
3294 format!("{back}"),
3295 format!("{wrapped}"),
3296 "round-trip lost information from the nested CssParsingError"
3297 );
3298 }
3299
3300 #[test]
3301 fn css_parse_error_inner_round_trips_for_every_variant() {
3302 let km = key_map();
3303 let margin =
3304 CombinedCssPropertyType::from_str("margin", &km).expect("margin is a shorthand");
3305
3306 let cases = vec![
3307 CssParseErrorInner::ParseError(CssSyntaxError::UnexpectedEndOfStream(
3308 CssSyntaxErrorPos { row: 0, col: 0 },
3309 )),
3310 CssParseErrorInner::ParseError(CssSyntaxError::InvalidAdvance(
3312 CssSyntaxInvalidAdvance {
3313 expected: isize::MIN,
3314 total: usize::MAX,
3315 pos: CssSyntaxErrorPos {
3316 row: usize::MAX,
3317 col: usize::MAX,
3318 },
3319 },
3320 )),
3321 CssParseErrorInner::ParseError(CssSyntaxError::UnsupportedToken(CssSyntaxErrorPos {
3322 row: 3,
3323 col: 7,
3324 })),
3325 CssParseErrorInner::UnclosedBlock,
3326 CssParseErrorInner::MalformedCss,
3327 CssParseErrorInner::DynamicCssParseError(DynamicCssParseError::InvalidBraceContents(
3328 "--x",
3329 )),
3330 CssParseErrorInner::PseudoSelectorParseError(
3331 CssPseudoSelectorParseError::EmptyNthChild,
3332 ),
3333 CssParseErrorInner::NodeTypeTag(NodeTypeTagParseError::Invalid("notatag")),
3334 CssParseErrorInner::UnknownPropertyKey("key", "value"),
3335 CssParseErrorInner::UnknownPropertyKey("", ""),
3336 CssParseErrorInner::VarOnShorthandProperty {
3337 key: margin,
3338 value: "var(--m)",
3339 },
3340 ];
3341
3342 for case in &cases {
3343 assert_eq!(
3344 &case.to_contained().to_shared(),
3345 case,
3346 "round-trip changed the value for {case:?}"
3347 );
3348 }
3349 }
3350
3351 #[test]
3352 fn css_parse_error_round_trips_including_unicode_payloads() {
3353 let css = "div { width: \u{1F600}; }";
3354 let err = CssParseError {
3355 css_string: css,
3356 error: CssParseErrorInner::UnknownPropertyKey("k\u{1F600}", "v\u{4E2D}"),
3357 location: loc(1, 2),
3358 };
3359 assert_eq!(err.to_contained().to_shared(), err);
3360
3361 let err = CssParseError {
3363 css_string: "",
3364 error: CssParseErrorInner::MalformedCss,
3365 location: ErrorLocationRange::default(),
3366 };
3367 assert_eq!(err.to_contained().to_shared(), err);
3368 }
3369
3370 #[test]
3371 fn css_path_parse_error_round_trips_for_every_variant() {
3372 let cases = vec![
3373 CssPathParseError::EmptyPath,
3374 CssPathParseError::InvalidTokenEncountered("{"),
3375 CssPathParseError::InvalidTokenEncountered(""),
3376 CssPathParseError::UnexpectedEndOfStream("div"),
3377 CssPathParseError::SyntaxError(CssSyntaxError::UnknownToken(CssSyntaxErrorPos {
3378 row: usize::MAX,
3379 col: 0,
3380 })),
3381 CssPathParseError::NodeTypeTag(NodeTypeTagParseError::Invalid("notatag")),
3382 CssPathParseError::PseudoSelectorParseError(
3383 CssPseudoSelectorParseError::InvalidNthChildPattern("2x"),
3384 ),
3385 ];
3386 for case in &cases {
3387 assert_eq!(
3388 &case.to_contained().to_shared(),
3389 case,
3390 "round-trip changed the value for {case:?}"
3391 );
3392 }
3393 }
3394
3395 #[test]
3396 fn css_parse_warn_msg_round_trips_for_every_variant() {
3397 let inners = vec![
3398 CssParseWarnMsgInner::UnsupportedKeyValuePair {
3399 key: "foo",
3400 value: "bar",
3401 },
3402 CssParseWarnMsgInner::UnsupportedKeyValuePair { key: "", value: "" },
3403 CssParseWarnMsgInner::ParseError(CssParseErrorInner::MalformedCss),
3404 CssParseWarnMsgInner::SkippedRule {
3405 selector: None,
3406 error: CssParseErrorInner::UnclosedBlock,
3407 },
3408 CssParseWarnMsgInner::SkippedRule {
3409 selector: Some("div"),
3410 error: CssParseErrorInner::MalformedCss,
3411 },
3412 CssParseWarnMsgInner::SkippedDeclaration {
3413 key: "width",
3414 value: "\u{1F600}",
3415 error: CssParseErrorInner::MalformedCss,
3416 },
3417 CssParseWarnMsgInner::MalformedStructure {
3418 message: "unclosed",
3419 },
3420 ];
3421
3422 for inner in &inners {
3423 assert_eq!(
3424 &inner.to_contained().to_shared(),
3425 inner,
3426 "round-trip changed the value for {inner:?}"
3427 );
3428
3429 let msg = CssParseWarnMsg {
3430 warning: inner.clone(),
3431 location: loc(7, 42),
3432 };
3433 let back = msg.to_contained();
3434 let back = back.to_shared();
3435 assert_eq!(back, msg, "CssParseWarnMsg round-trip changed the value");
3436 assert_eq!(back.location, loc(7, 42), "location was not preserved");
3437 }
3438 }
3439
3440 #[test]
3441 fn unparsed_css_rule_block_round_trips() {
3442 let mut declarations = BTreeMap::new();
3443 declarations.insert("width", ("100px", loc(1, 2)));
3444 declarations.insert("color", ("\u{1F600}", loc(3, 4)));
3445
3446 let block = UnparsedCssRuleBlock {
3447 path: CssPath {
3448 selectors: vec![
3449 CssPathSelector::Global,
3450 CssPathSelector::Class("btn".to_string().into()),
3451 ]
3452 .into(),
3453 },
3454 declarations,
3455 conditions: vec![DynamicSelector::Media(MediaType::Screen)],
3459 };
3460
3461 assert_eq!(block.to_contained().to_shared(), block);
3462
3463 let empty = UnparsedCssRuleBlock {
3465 path: CssPath {
3466 selectors: Vec::new().into(),
3467 },
3468 declarations: BTreeMap::new(),
3469 conditions: Vec::new(),
3470 };
3471 assert_eq!(empty.to_contained().to_shared(), empty);
3472 }
3473
3474 #[test]
3479 fn new_from_str_valid_minimal() {
3480 let (css, warnings) = new_from_str("div { width: 100px; }");
3481 assert_eq!(css.rules.len(), 1);
3482 assert_eq!(css.rules.as_slice()[0].declarations.len(), 1);
3483 assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
3484 }
3485
3486 #[test]
3487 fn new_from_str_empty_input_yields_an_empty_stylesheet() {
3488 let (css, warnings) = new_from_str("");
3489 assert_eq!(css.rules.len(), 0);
3490 assert!(warnings.is_empty());
3491
3492 let (css, _warnings) = new_from_str(" \n\t ");
3493 assert_eq!(css.rules.len(), 0);
3494 }
3495
3496 #[test]
3497 fn new_from_str_unclosed_block_warns_instead_of_failing() {
3498 let (css, warnings) = new_from_str("div { width: 100px;");
3499 assert_eq!(css.rules.len(), 0, "an unclosed block emits no rules");
3500 assert!(
3501 warnings.iter().any(|w| matches!(
3502 w.warning,
3503 CssParseWarnMsgInner::MalformedStructure { .. }
3504 )),
3505 "expected a MalformedStructure warning, got {warnings:?}"
3506 );
3507 }
3508
3509 #[test]
3510 fn new_from_str_unknown_property_is_a_warning_not_a_dropped_rule() {
3511 let (css, warnings) = new_from_str("div { definitely-not-a-property: 1; width: 10px; }");
3512 assert_eq!(css.rules.len(), 1);
3513 assert_eq!(css.rules.as_slice()[0].declarations.len(), 1);
3515 assert!(!warnings.is_empty(), "the unknown key should have warned");
3516 }
3517
3518 #[test]
3520 fn new_from_str_hostile_input_never_panics() {
3521 let long_rule = "div { width: 100px; }".repeat(2_000);
3522 let deep_nesting = format!("{}{}", "div {".repeat(500), "}".repeat(500));
3523 let unbalanced_open = "{".repeat(5_000);
3524 let unbalanced_close = "}".repeat(5_000);
3525 let long_selector = format!("{} {{ width: 1px; }}", "div ".repeat(10_000));
3526
3527 let mut inputs: Vec<&str> = HOSTILE.to_vec();
3528 inputs.push(&long_rule);
3529 inputs.push(&deep_nesting);
3530 inputs.push(&unbalanced_open);
3531 inputs.push(&unbalanced_close);
3532 inputs.push(&long_selector);
3533 inputs.push("div { width: \u{1F600}; }");
3534 inputs.push("\u{1F600} { \u{4E2D}: \u{6587}; }");
3535 inputs.push("div { width: 100px; /* unterminated");
3536 inputs.push("@media (min-width: 800px) { div { width: 1px; } }");
3537 inputs.push("@theme(dark) { div { width: 1px; } }");
3538 inputs.push("@lang(\"de-DE\") { div { width: 1px; } }");
3539 inputs.push("@container sidebar (min-width: 400px) { div { width: 1px; } }");
3540 inputs.push("@definitely-not-an-at-rule x { div { width: 1px; } }");
3541 inputs.push(".a { .b { :hover { width: 1px; } } }");
3542 inputs.push("div[data-x=\"y\"] { width: 1px; }");
3543 inputs.push("div[ { width: 1px; }");
3544 inputs.push("a, b, , c { width: 1px; }");
3545 inputs.push("div:nth-child(999999999999) { width: 1px; }");
3546
3547 for input in inputs {
3548 let r = catch(|| {
3549 let (css, warnings) = new_from_str(input);
3550 (css.rules.len(), warnings.len())
3551 });
3552 assert!(
3553 r.is_ok(),
3554 "new_from_str({:.60?}) panicked despite the `Never panics` contract: {}",
3555 input,
3556 r.unwrap_err()
3557 );
3558 }
3559 }
3560
3561 #[test]
3562 fn new_from_str_at_rules_attach_conditions_to_nested_rules() {
3563 let (css, _warnings) = new_from_str("@media screen { div { width: 1px; } }");
3564 assert_eq!(css.rules.len(), 1);
3565 let rule = &css.rules.as_slice()[0];
3566 assert!(
3567 rule.conditions
3568 .as_slice()
3569 .contains(&DynamicSelector::Media(MediaType::Screen)),
3570 "the @media condition was not attached: {:?}",
3571 rule.conditions.as_slice()
3572 );
3573 }
3574
3575 #[test]
3576 fn new_from_str_comma_separated_selectors_emit_one_rule_each() {
3577 let (css, _warnings) = new_from_str("div, p { width: 1px; }");
3578 assert_eq!(css.rules.len(), 2, "each selector in the list gets its own rule");
3579 }
3580
3581 #[test]
3582 fn new_from_str_inner_matches_new_from_str() {
3583 let css_string = "div { width: 100px; }";
3584 let mut tokenizer = Tokenizer::new(css_string);
3585 let (rules, warnings) = new_from_str_inner(css_string, &mut tokenizer);
3586 assert_eq!(rules.len(), 1);
3587 assert!(warnings.is_empty());
3588 }
3589
3590 #[test]
3591 fn get_error_location_tracks_the_tokenizer_position() {
3592 let css_string = "div { width: 100px; }";
3593 let mut tokenizer = Tokenizer::new(css_string);
3594 assert_eq!(get_error_location(&tokenizer).original_pos, 0);
3595
3596 let _ = tokenizer.parse_next();
3597 let after = get_error_location(&tokenizer).original_pos;
3598 assert!(after > 0, "the tokenizer position did not advance");
3599 assert!(
3600 after <= css_string.len(),
3601 "the tokenizer position ran past the end of the input"
3602 );
3603
3604 let empty = Tokenizer::new("");
3606 assert_eq!(get_error_location(&empty).original_pos, 0);
3607 }
3608
3609 #[test]
3610 fn css_blocks_to_stylesheet_parses_known_keys_and_warns_on_unknown_ones() {
3611 let css_string = "div { width: 100px; }";
3612
3613 let mut declarations = BTreeMap::new();
3614 declarations.insert("width", ("100px", loc(6, 18)));
3615 let good = UnparsedCssRuleBlock {
3616 path: CssPath {
3617 selectors: vec![CssPathSelector::Global].into(),
3618 },
3619 declarations,
3620 conditions: Vec::new(),
3621 };
3622
3623 let mut declarations = BTreeMap::new();
3624 declarations.insert("definitely-not-a-property", ("1", loc(0, 1)));
3625 let bad = UnparsedCssRuleBlock {
3626 path: CssPath {
3627 selectors: vec![CssPathSelector::Global].into(),
3628 },
3629 declarations,
3630 conditions: Vec::new(),
3631 };
3632
3633 let (rules, warnings) = css_blocks_to_stylesheet(vec![good, bad], css_string);
3634 assert_eq!(rules.len(), 2, "both blocks are emitted");
3635 assert_eq!(rules[0].declarations.len(), 1);
3636 assert_eq!(rules[1].declarations.len(), 0, "the unknown key is dropped");
3637 assert_eq!(warnings.len(), 1, "the unknown key produced exactly one warning");
3638 assert!(matches!(
3639 warnings[0].warning,
3640 CssParseWarnMsgInner::SkippedDeclaration { .. }
3641 ));
3642 }
3643
3644 #[test]
3645 fn css_blocks_to_stylesheet_empty_input_is_empty_output() {
3646 let (rules, warnings) = css_blocks_to_stylesheet(Vec::new(), "");
3647 assert!(rules.is_empty());
3648 assert!(warnings.is_empty());
3649 }
3650}