1#[derive(Debug, Clone, Copy)]
4pub struct CssKeywordText<'a> {
5 text: &'a str,
6}
7
8pub const fn css_keyword(text: &str) -> CssKeywordText<'_> {
10 CssKeywordText { text }
11}
12
13impl<'a> CssKeywordText<'a> {
14 pub fn equals(self, expected: &str) -> bool {
16 self.text.eq_ignore_ascii_case(expected)
17 }
18
19 pub fn strip_prefix(self, expected: &str) -> Option<&'a str> {
21 let prefix = self.text.get(..expected.len())?;
22 prefix
23 .eq_ignore_ascii_case(expected)
24 .then(|| &self.text[expected.len()..])
25 }
26
27 pub fn strip_suffix(self, expected: &str) -> Option<&'a str> {
29 let suffix_start = self.text.len().checked_sub(expected.len())?;
30 let suffix = self.text.get(suffix_start..)?;
31 suffix
32 .eq_ignore_ascii_case(expected)
33 .then(|| &self.text[..suffix_start])
34 }
35
36 pub fn find(self, expected: &str) -> Option<usize> {
38 if expected.is_empty() {
39 return Some(0);
40 }
41 self.text
42 .as_bytes()
43 .windows(expected.len())
44 .position(|candidate| candidate.eq_ignore_ascii_case(expected.as_bytes()))
45 }
46
47 pub fn contains(self, expected: &str) -> bool {
49 self.find(expected).is_some()
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::css_keyword;
56
57 #[test]
58 fn compares_css_keywords_without_allocating_lowercase_copies() {
59 assert!(css_keyword("@LaYeR").equals("@layer"));
60 assert!(!css_keyword("@layered").equals("@layer"));
61 }
62
63 #[test]
64 fn strips_only_complete_ascii_case_insensitive_affixes() {
65 assert_eq!(
66 css_keyword("@KEYFRAMES fade").strip_prefix("@keyframes"),
67 Some(" fade")
68 );
69 assert_eq!(
70 css_keyword("red !IMPORTANT").strip_suffix("!important"),
71 Some("red ")
72 );
73 assert_eq!(css_keyword("@lay").strip_prefix("@layer"), None);
74 assert_eq!(css_keyword("@FORWARD 'x' AS ui-*").find(" as "), Some(12));
75 assert!(css_keyword("media:@SuPpOrTs").contains("@supports"));
76 }
77}