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