1use std::{borrow::Cow, fmt};
2
3#[derive(Debug, Clone)]
16pub struct ClassNameV0 {
17 raw: String,
18 decoded: Option<String>,
19}
20
21impl ClassNameV0 {
22 pub fn new(raw: impl Into<String>) -> Self {
23 let raw = raw.into();
24 let decoded = match decode_css_identifier_escapes(&raw) {
25 Cow::Borrowed(_) => None,
26 Cow::Owned(decoded) => Some(decoded),
27 };
28 Self { raw, decoded }
29 }
30
31 pub fn raw(&self) -> &str {
32 &self.raw
33 }
34
35 pub fn decoded(&self) -> &str {
36 self.decoded.as_deref().unwrap_or(&self.raw)
37 }
38
39 pub fn into_raw(self) -> String {
40 self.raw
41 }
42
43 pub fn same_as(&self, other: &Self) -> bool {
44 self.decoded() == other.decoded()
45 }
46
47 pub fn canonical_key(self) -> CanonicalClassKeyV0 {
48 let decoded = self.decoded.unwrap_or(self.raw);
49 CanonicalClassKeyV0(decoded, CanonicalClassKeySealV0(()))
50 }
51
52 fn from_plain(raw: &str) -> Self {
53 Self {
54 raw: raw.to_owned(),
55 decoded: None,
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61struct CanonicalClassKeySealV0(());
62
63#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
64pub struct CanonicalClassKeyV0(String, CanonicalClassKeySealV0);
65
66impl CanonicalClassKeyV0 {
67 pub fn as_str(&self) -> &str {
68 &self.0
69 }
70}
71
72impl serde::Serialize for CanonicalClassKeyV0 {
73 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
74 where
75 S: serde::Serializer,
76 {
77 serializer.serialize_str(self.as_str())
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
82struct CanonicalIdKeySealV0(());
83
84#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
86pub struct CanonicalIdKeyV0(String, CanonicalIdKeySealV0);
87
88impl CanonicalIdKeyV0 {
89 pub fn from_authored(authored: &str) -> Self {
90 Self(
91 decode_css_identifier_escapes(authored).into_owned(),
92 CanonicalIdKeySealV0(()),
93 )
94 }
95
96 pub fn as_str(&self) -> &str {
97 &self.0
98 }
99}
100
101impl serde::Serialize for CanonicalIdKeyV0 {
102 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
103 where
104 S: serde::Serializer,
105 {
106 serializer.serialize_str(self.as_str())
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
111struct CanonicalTypeSelectorKeySealV0(());
112
113#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
115pub struct CanonicalTypeSelectorKeyV0(String, CanonicalTypeSelectorKeySealV0);
116
117impl CanonicalTypeSelectorKeyV0 {
118 pub fn from_authored(authored: &str) -> Self {
119 Self(
120 decode_css_identifier_escapes(authored).into_owned(),
121 CanonicalTypeSelectorKeySealV0(()),
122 )
123 }
124
125 pub fn as_str(&self) -> &str {
126 &self.0
127 }
128}
129
130impl serde::Serialize for CanonicalTypeSelectorKeyV0 {
131 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
132 where
133 S: serde::Serializer,
134 {
135 serializer.serialize_str(self.as_str())
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum PropertyNameKindV0 {
143 Standard,
144 Custom,
145}
146
147#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
252#[serde(transparent)]
253pub struct AuthoredPropertyTextV0(String);
254
255impl AuthoredPropertyTextV0 {
256 pub fn new(authored: impl Into<String>) -> Self {
257 Self(authored.into())
258 }
259
260 pub fn write_into(&self, out: &mut impl fmt::Write) -> fmt::Result {
263 out.write_str(&self.0)
264 }
265
266 pub fn is_empty(&self) -> bool {
267 self.0.is_empty()
268 }
269
270 pub fn to_property_name(&self) -> PropertyNameV0 {
271 PropertyNameV0::from_authored(self.0.clone())
272 }
273
274 pub fn to_custom_key(&self) -> CanonicalCustomPropertyNameV0 {
275 PropertyNameV0::canonical_custom_key(self.0.clone())
276 }
277
278 pub fn to_standard_key(&self) -> CanonicalStandardPropertyNameV0 {
279 PropertyNameV0::canonical_standard_key(self.0.clone())
280 }
281}
282
283pub fn render_authored(authored: &AuthoredPropertyTextV0, output: &mut String) -> fmt::Result {
289 authored.write_into(output)
290}
291
292#[derive(Debug, Clone)]
323#[allow(private_interfaces)]
324pub enum PropertyNameV0 {
325 Standard(StandardPropertyNamePayloadV0),
326 Custom(CustomPropertyNamePayloadV0),
327}
328
329#[derive(Debug, Clone)]
330#[allow(dead_code)]
331struct StandardPropertyNamePayloadV0 {
332 authored: AuthoredPropertyTextV0,
333 decoded: String,
334 canonical: CanonicalStandardPropertyNameV0,
335}
336
337#[derive(Debug, Clone)]
338#[allow(dead_code)]
339struct CustomPropertyNamePayloadV0 {
340 authored: AuthoredPropertyTextV0,
341 decoded: String,
342 canonical: CanonicalCustomPropertyNameV0,
343}
344
345impl PropertyNameV0 {
346 pub fn from_authored(authored: impl Into<String>) -> Self {
349 let authored = authored.into();
350 let authored = authored.trim().to_string();
351 let decoded = decode_css_identifier_escapes(&authored).into_owned();
352 let kind = if decoded.starts_with("--") {
353 PropertyNameKindV0::Custom
354 } else {
355 PropertyNameKindV0::Standard
356 };
357 Self::from_decoded(authored, decoded, kind)
358 }
359
360 pub fn new(authored: impl Into<String>, kind: PropertyNameKindV0) -> Self {
361 let authored = authored.into();
362 let authored = authored.trim().to_string();
363 let decoded = decode_css_identifier_escapes(&authored).into_owned();
364 Self::from_decoded(authored, decoded, kind)
365 }
366
367 fn from_decoded(authored: String, decoded: String, kind: PropertyNameKindV0) -> Self {
368 match kind {
369 PropertyNameKindV0::Standard => Self::Standard(StandardPropertyNamePayloadV0 {
370 canonical: CanonicalStandardPropertyNameV0(
371 decoded.to_ascii_lowercase(),
372 CanonicalStandardPropertyNameSealV0(()),
373 ),
374 authored: AuthoredPropertyTextV0::new(authored),
375 decoded,
376 }),
377 PropertyNameKindV0::Custom => Self::Custom(CustomPropertyNamePayloadV0 {
378 canonical: CanonicalCustomPropertyNameV0(
379 decoded.clone(),
380 CanonicalCustomPropertyNameSealV0(()),
381 ),
382 authored: AuthoredPropertyTextV0::new(authored),
383 decoded,
384 }),
385 }
386 }
387
388 pub fn standard(authored: impl Into<String>) -> Self {
389 Self::new(authored, PropertyNameKindV0::Standard)
390 }
391
392 pub fn custom(authored: impl Into<String>) -> Self {
393 Self::new(authored, PropertyNameKindV0::Custom)
394 }
395
396 pub fn kind(&self) -> PropertyNameKindV0 {
397 match self {
398 Self::Standard(_) => PropertyNameKindV0::Standard,
399 Self::Custom(_) => PropertyNameKindV0::Custom,
400 }
401 }
402
403 pub fn authored_text(&self) -> AuthoredPropertyTextV0 {
404 match self {
405 Self::Standard(payload) => payload.authored.clone(),
406 Self::Custom(payload) => payload.authored.clone(),
407 }
408 }
409
410 pub fn canonical_name(&self) -> &str {
411 match self {
412 Self::Standard(payload) => payload.canonical.as_str(),
413 Self::Custom(payload) => payload.canonical.as_str(),
414 }
415 }
416
417 pub fn same_as(&self, other: &Self) -> bool {
418 match (self, other) {
419 (Self::Standard(left), Self::Standard(right)) => left.canonical == right.canonical,
420 (Self::Custom(left), Self::Custom(right)) => left.canonical == right.canonical,
421 _ => false,
422 }
423 }
424
425 pub fn canonical_key(&self) -> CanonicalPropertyKeyV0 {
426 match self {
427 Self::Standard(payload) => CanonicalPropertyKeyV0::Standard(payload.canonical.clone()),
428 Self::Custom(payload) => CanonicalPropertyKeyV0::Custom(payload.canonical.clone()),
429 }
430 }
431
432 pub fn as_custom_key(&self) -> Option<CanonicalCustomPropertyNameV0> {
433 match self {
434 Self::Custom(payload) => Some(payload.canonical.clone()),
435 Self::Standard(_) => None,
436 }
437 }
438
439 pub fn as_standard_key(&self) -> Option<&CanonicalStandardPropertyNameV0> {
440 match self {
441 Self::Standard(payload) => Some(&payload.canonical),
442 Self::Custom(_) => None,
443 }
444 }
445
446 pub fn canonical_custom_key(authored: impl Into<String>) -> CanonicalCustomPropertyNameV0 {
447 match Self::custom(authored) {
448 Self::Custom(payload) => payload.canonical,
449 Self::Standard(_) => unreachable!("custom constructor returned a standard name"),
450 }
451 }
452
453 pub fn canonical_standard_key(authored: impl Into<String>) -> CanonicalStandardPropertyNameV0 {
454 match Self::standard(authored) {
455 Self::Standard(payload) => payload.canonical,
456 Self::Custom(_) => unreachable!("standard constructor returned a custom name"),
457 }
458 }
459}
460
461#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
462struct CanonicalStandardPropertyNameSealV0(());
463
464#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
478pub struct CanonicalStandardPropertyNameV0(String, CanonicalStandardPropertyNameSealV0);
479
480impl CanonicalStandardPropertyNameV0 {
481 pub fn as_str(&self) -> &str {
482 &self.0
483 }
484}
485
486impl serde::Serialize for CanonicalStandardPropertyNameV0 {
487 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
488 where
489 S: serde::Serializer,
490 {
491 serializer.serialize_str(self.as_str())
492 }
493}
494
495impl<'de> serde::Deserialize<'de> for CanonicalStandardPropertyNameV0 {
496 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
497 where
498 D: serde::Deserializer<'de>,
499 {
500 let authored = <String as serde::Deserialize>::deserialize(deserializer)?;
501 Ok(PropertyNameV0::canonical_standard_key(authored))
502 }
503}
504
505#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
506struct CanonicalCustomPropertyNameSealV0(());
507
508#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
519pub struct CanonicalCustomPropertyNameV0(String, CanonicalCustomPropertyNameSealV0);
520
521impl CanonicalCustomPropertyNameV0 {
522 pub fn as_str(&self) -> &str {
523 &self.0
524 }
525}
526
527impl serde::Serialize for CanonicalCustomPropertyNameV0 {
528 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
529 where
530 S: serde::Serializer,
531 {
532 serializer.serialize_str(self.as_str())
533 }
534}
535
536impl<'de> serde::Deserialize<'de> for CanonicalCustomPropertyNameV0 {
537 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
538 where
539 D: serde::Deserializer<'de>,
540 {
541 let authored = <String as serde::Deserialize>::deserialize(deserializer)?;
542 Ok(PropertyNameV0::canonical_custom_key(authored))
543 }
544}
545
546#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
547pub enum CanonicalPropertyKeyV0 {
548 Standard(CanonicalStandardPropertyNameV0),
549 Custom(CanonicalCustomPropertyNameV0),
550}
551
552impl CanonicalPropertyKeyV0 {
553 pub fn as_str(&self) -> &str {
554 match self {
555 Self::Standard(name) => name.as_str(),
556 Self::Custom(name) => name.as_str(),
557 }
558 }
559
560 pub fn kind(&self) -> PropertyNameKindV0 {
561 match self {
562 Self::Standard(_) => PropertyNameKindV0::Standard,
563 Self::Custom(_) => PropertyNameKindV0::Custom,
564 }
565 }
566
567 pub fn as_custom(&self) -> Option<&CanonicalCustomPropertyNameV0> {
568 match self {
569 Self::Custom(name) => Some(name),
570 Self::Standard(_) => None,
571 }
572 }
573
574 pub fn as_standard(&self) -> Option<&CanonicalStandardPropertyNameV0> {
575 match self {
576 Self::Standard(name) => Some(name),
577 Self::Custom(_) => None,
578 }
579 }
580}
581
582impl serde::Serialize for CanonicalPropertyKeyV0 {
583 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
584 where
585 S: serde::Serializer,
586 {
587 serializer.serialize_str(self.as_str())
588 }
589}
590
591impl<'de> serde::Deserialize<'de> for CanonicalPropertyKeyV0 {
592 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
593 where
594 D: serde::Deserializer<'de>,
595 {
596 let authored = <String as serde::Deserialize>::deserialize(deserializer)?;
597 Ok(PropertyNameV0::from_authored(authored).canonical_key())
598 }
599}
600
601pub fn property_names_same(left: &str, right: &str) -> bool {
604 PropertyNameV0::from_authored(left).same_as(&PropertyNameV0::from_authored(right))
605}
606
607pub fn is_custom_property_name(authored: &str) -> bool {
610 PropertyNameV0::from_authored(authored).kind() == PropertyNameKindV0::Custom
611}
612
613#[derive(Debug, Clone, Copy, PartialEq, Eq)]
614pub struct ClassSelectorPositionV0 {
615 pub start: usize,
616 pub end: usize,
617}
618
619#[derive(Debug, Clone)]
620pub struct ClassSelectorNameV0 {
621 pub name: ClassNameV0,
622 pub position: ClassSelectorPositionV0,
623}
624
625pub fn is_css_name_start(ch: char) -> bool {
627 ch == '-' || ch == '_' || ch.is_ascii_alphabetic() || !ch.is_ascii()
628}
629
630pub fn is_css_name_continue(ch: char) -> bool {
632 is_css_name_start(ch) || ch.is_ascii_digit()
633}
634
635pub fn is_ascii_word_continue(ch: char) -> bool {
641 ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-')
642}
643
644pub fn is_safe_css_identifier(value: &str) -> bool {
645 let mut characters = value.chars();
646 let Some(first) = characters.next() else {
647 return false;
648 };
649 match first {
650 character
651 if character == '_' || character.is_ascii_alphabetic() || !character.is_ascii() => {}
652 '-' => {
653 let Some(second) = characters.next() else {
654 return false;
655 };
656 if !(second == '-'
657 || second == '_'
658 || second.is_ascii_alphabetic()
659 || !second.is_ascii())
660 {
661 return false;
662 }
663 }
664 _ => return false,
665 }
666 characters.all(is_css_name_continue)
667}
668
669pub fn decode_css_identifier_escapes(text: &str) -> Cow<'_, str> {
670 if !text.contains('\\') {
671 return Cow::Borrowed(text);
672 }
673
674 let mut output = String::with_capacity(text.len());
675 let mut index = 0usize;
676 while index < text.len() {
677 let Some(ch) = text[index..].chars().next() else {
678 break;
679 };
680 if ch != '\\' {
681 output.push(ch);
682 index += ch.len_utf8();
683 continue;
684 }
685
686 let escape_start = index;
687 index += ch.len_utf8();
688 let Some(next) = text[index..].chars().next() else {
689 output.push(char::REPLACEMENT_CHARACTER);
690 break;
691 };
692 if is_css_newline(next) {
693 output.push_str(&text[escape_start..index + next.len_utf8()]);
694 index += next.len_utf8();
695 continue;
696 }
697 if next.is_ascii_hexdigit() {
698 let hex_start = index;
699 let mut hex_end = index;
700 let mut digit_count = 0usize;
701 while hex_end < text.len() && digit_count < 6 {
702 let Some(candidate) = text[hex_end..].chars().next() else {
703 break;
704 };
705 if !candidate.is_ascii_hexdigit() {
706 break;
707 }
708 hex_end += candidate.len_utf8();
709 digit_count += 1;
710 }
711 let codepoint = u32::from_str_radix(&text[hex_start..hex_end], 16).ok();
712 output.push(
713 codepoint
714 .filter(|value| *value != 0)
715 .and_then(char::from_u32)
716 .unwrap_or(char::REPLACEMENT_CHARACTER),
717 );
718 index = hex_end;
719 if let Some(terminator) = text[index..].chars().next()
720 && terminator.is_ascii_whitespace()
721 {
722 index += terminator.len_utf8();
723 }
724 continue;
725 }
726
727 output.push(next);
728 index += next.len_utf8();
729 }
730
731 Cow::Owned(output)
732}
733
734pub fn class_selector_name_end(text: &str, start: usize) -> Option<usize> {
735 let first = text.get(start..)?.chars().next()?;
736 let mut index = if first == '\\' {
737 css_identifier_escape_sequence_end(text, start)?
738 } else if is_css_name_start(first) {
739 start + first.len_utf8()
740 } else {
741 return None;
742 };
743
744 while index < text.len() {
745 let Some(ch) = text[index..].chars().next() else {
746 break;
747 };
748 if ch == '\\' {
749 let Some(end) = css_identifier_escape_sequence_end(text, index) else {
750 break;
751 };
752 index = end;
753 } else if is_css_name_continue(ch) {
754 index += ch.len_utf8();
755 } else {
756 break;
757 }
758 }
759 Some(index)
760}
761
762pub fn class_selector_names(selector: &str) -> Vec<ClassSelectorNameV0> {
763 if let Some(names) = ascii_class_selector_names(selector) {
764 return names;
765 }
766 general_class_selector_names(selector)
767}
768
769fn ascii_class_selector_names(selector: &str) -> Option<Vec<ClassSelectorNameV0>> {
770 let bytes = selector.as_bytes();
771 let mut names = Vec::new();
772 let mut index = 0usize;
773 let mut paren_depth = 0usize;
774 let mut bracket_depth = 0usize;
775 let mut quote = None;
776
777 while index < bytes.len() {
778 let byte = bytes[index];
779 if !byte.is_ascii() || byte == b'\\' {
780 return None;
781 }
782 if let Some(active_quote) = quote {
783 if byte == active_quote {
784 quote = None;
785 }
786 index += 1;
787 continue;
788 }
789 match byte {
790 b'"' | b'\'' => quote = Some(byte),
791 b'(' => paren_depth += 1,
792 b')' => paren_depth = paren_depth.saturating_sub(1),
793 b'[' => bracket_depth += 1,
794 b']' => bracket_depth = bracket_depth.saturating_sub(1),
795 b'.' if paren_depth == 0 && bracket_depth == 0 => {
796 let start = index + 1;
797 let Some(first) = bytes.get(start).copied() else {
798 index += 1;
799 continue;
800 };
801 if !ascii_css_name_start(first) {
802 index += 1;
803 continue;
804 }
805 let mut end = start + 1;
806 while end < bytes.len() && ascii_css_name_continue(bytes[end]) {
807 end += 1;
808 }
809 names.push(ClassSelectorNameV0 {
810 name: ClassNameV0::from_plain(&selector[start..end]),
811 position: ClassSelectorPositionV0 { start, end },
812 });
813 index = end;
814 continue;
815 }
816 _ => {}
817 }
818 index += 1;
819 }
820 Some(names)
821}
822
823fn ascii_css_name_start(byte: u8) -> bool {
824 matches!(byte, b'-' | b'_') || byte.is_ascii_alphabetic()
825}
826
827fn ascii_css_name_continue(byte: u8) -> bool {
828 ascii_css_name_start(byte) || byte.is_ascii_digit()
829}
830
831fn general_class_selector_names(selector: &str) -> Vec<ClassSelectorNameV0> {
832 let mut names = Vec::new();
833 let mut index = 0usize;
834 let mut paren_depth = 0usize;
835 let mut bracket_depth = 0usize;
836 let mut quote = None;
837
838 while index < selector.len() {
839 let Some(ch) = selector[index..].chars().next() else {
840 break;
841 };
842 if ch == '\\' {
843 index = css_identifier_escape_sequence_end(selector, index)
844 .unwrap_or(index + ch.len_utf8());
845 continue;
846 }
847 if let Some(active_quote) = quote {
848 if ch == active_quote {
849 quote = None;
850 }
851 index += ch.len_utf8();
852 continue;
853 }
854 match ch {
855 '"' | '\'' => quote = Some(ch),
856 '(' => paren_depth += 1,
857 ')' => paren_depth = paren_depth.saturating_sub(1),
858 '[' => bracket_depth += 1,
859 ']' => bracket_depth = bracket_depth.saturating_sub(1),
860 '.' if paren_depth == 0 && bracket_depth == 0 => {
861 let start = index + ch.len_utf8();
862 if let Some(end) = class_selector_name_end(selector, start) {
863 names.push(ClassSelectorNameV0 {
864 name: ClassNameV0::new(&selector[start..end]),
865 position: ClassSelectorPositionV0 { start, end },
866 });
867 index = end;
868 continue;
869 }
870 }
871 _ => {}
872 }
873 index += ch.len_utf8();
874 }
875
876 names
877}
878
879pub fn css_identifier_escape_sequence_end(text: &str, slash_index: usize) -> Option<usize> {
883 if text[slash_index..].chars().next()? != '\\' {
884 return None;
885 }
886 let mut index = slash_index + '\\'.len_utf8();
887 let next = text[index..].chars().next()?;
888 if is_css_newline(next) {
889 return None;
890 }
891 if !next.is_ascii_hexdigit() {
892 return Some(index + next.len_utf8());
893 }
894
895 let mut digit_count = 0usize;
896 while index < text.len() && digit_count < 6 {
897 let Some(candidate) = text[index..].chars().next() else {
898 break;
899 };
900 if !candidate.is_ascii_hexdigit() {
901 break;
902 }
903 index += candidate.len_utf8();
904 digit_count += 1;
905 }
906 if let Some(terminator) = text[index..].chars().next()
907 && terminator.is_ascii_whitespace()
908 {
909 index += terminator.len_utf8();
910 }
911 Some(index)
912}
913
914fn is_css_newline(ch: char) -> bool {
915 matches!(ch, '\n' | '\r' | '\u{c}')
916}
917
918#[cfg(test)]
919mod tests {
920 use super::*;
921
922 fn authored_text(property: &PropertyNameV0) -> String {
923 let mut output = String::new();
924 let result = property.authored_text().write_into(&mut output);
925 assert!(result.is_ok(), "writing into a String must not fail");
926 output
927 }
928
929 #[test]
930 fn decodes_css_escapes_without_changing_plain_names() {
931 assert!(matches!(
934 decode_css_identifier_escapes("plain"),
935 Cow::Borrowed("plain")
936 ));
937 assert_eq!(decode_css_identifier_escapes(r"a\.b"), "a.b");
938 assert_eq!(decode_css_identifier_escapes(r"\31 23"), "123");
939 assert_eq!(decode_css_identifier_escapes(r"\0"), "\u{fffd}");
940 assert_eq!(decode_css_identifier_escapes("\\"), "\u{fffd}");
941 assert_eq!(decode_css_identifier_escapes("\\\n"), "\\\n");
942 }
943
944 #[test]
945 fn identifier_escape_boundaries_reject_newline_and_end_of_input() {
946 assert_eq!(css_identifier_escape_sequence_end(r"\31 23", 0), Some(4));
947 assert_eq!(css_identifier_escape_sequence_end(r"\:", 0), Some(2));
948 assert_eq!(css_identifier_escape_sequence_end("\\\n", 0), None);
949 assert_eq!(css_identifier_escape_sequence_end("\\", 0), None);
950 }
951
952 #[test]
953 fn class_name_identity_is_decoded_but_raw_text_is_preserved() {
954 let escaped = ClassNameV0::new(r"a\.b");
955 let plain = ClassNameV0::new("a.b");
956
957 assert!(escaped.same_as(&plain));
960 assert_eq!(escaped.raw(), r"a\.b");
961 assert_eq!(escaped.canonical_key().as_str(), "a.b");
962 }
963
964 #[test]
965 fn property_name_identity_preserves_custom_case_and_standard_case_folding() {
966 let custom_upper = PropertyNameV0::custom("--FOO");
967 let custom_lower = PropertyNameV0::custom("--foo");
968 let standard_upper = PropertyNameV0::standard("COLOR");
969 let standard_lower = PropertyNameV0::standard("color");
970
971 assert!(!custom_upper.same_as(&custom_lower));
972 assert!(standard_upper.same_as(&standard_lower));
973 assert_eq!(authored_text(&custom_upper), "--FOO");
974 assert_eq!(custom_upper.canonical_name(), "--FOO");
975 assert_eq!(authored_text(&standard_upper), "COLOR");
976 assert_eq!(standard_upper.canonical_name(), "color");
977 }
978
979 #[test]
980 fn custom_property_identity_decodes_escapes_without_destroying_authored_spelling() {
981 let escaped = PropertyNameV0::custom(r"--f\6f o");
982 let plain = PropertyNameV0::custom("--foo");
983
984 assert!(escaped.same_as(&plain));
985 assert_eq!(authored_text(&escaped), r"--f\6f o");
986 assert_eq!(escaped.canonical_name(), "--foo");
987 assert_eq!(
988 escaped
989 .as_custom_key()
990 .as_ref()
991 .map(CanonicalCustomPropertyNameV0::as_str),
992 Some("--foo")
993 );
994 }
995
996 #[test]
997 fn authored_text_serde_roundtrip_preserves_untrimmed_wire_bytes() {
998 let authored = serde_json::from_str::<AuthoredPropertyTextV0>(r#"" --f\\6f o ""#);
999 assert!(
1000 authored.is_ok(),
1001 "transparent authored text should deserialize"
1002 );
1003 let Ok(authored) = authored else {
1004 return;
1005 };
1006 let serialized = serde_json::to_string(&authored);
1007 assert!(serialized.is_ok(), "authored text should serialize");
1008
1009 assert_eq!(serialized.ok().as_deref(), Some(r#"" --f\\6f o ""#));
1010 assert_eq!(authored.to_custom_key().as_str(), "--foo");
1011 }
1012
1013 #[test]
1014 fn sealed_key_deserialization_preserves_forced_and_inferred_kind_rules() {
1015 let forced = serde_json::from_str::<CanonicalCustomPropertyNameV0>(r#""color""#);
1016 let inferred = serde_json::from_str::<CanonicalPropertyKeyV0>(r#""color""#);
1017 assert!(forced.is_ok(), "custom key should deserialize");
1018 assert!(inferred.is_ok(), "mixed key should deserialize");
1019 let (Ok(forced), Ok(inferred)) = (forced, inferred) else {
1020 return;
1021 };
1022
1023 assert_eq!(forced.as_str(), "color");
1024 assert_eq!(inferred.kind(), PropertyNameKindV0::Standard);
1025 assert_eq!(inferred.as_str(), "color");
1026 }
1027
1028 #[test]
1029 fn property_name_kind_is_classified_after_escape_decoding() {
1030 let property = PropertyNameV0::from_authored(r"\2d\2d FOO");
1031
1032 assert_eq!(property.kind(), PropertyNameKindV0::Custom);
1033 assert_eq!(authored_text(&property), r"\2d\2d FOO");
1034 assert_eq!(property.canonical_name(), "--FOO");
1035 assert!(is_custom_property_name(r"\2d\2d FOO"));
1036 assert!(!is_custom_property_name("COLOR"));
1037 }
1038
1039 #[test]
1040 fn ascii_class_scanner_matches_the_general_authority() {
1041 let selector = r#".card .title[data-x="a.b"]:is(.nested).plain"#;
1042 let summarize = |names: Vec<ClassSelectorNameV0>| {
1043 names
1044 .into_iter()
1045 .map(|entry| {
1046 (
1047 entry.name.into_raw(),
1048 entry.position.start,
1049 entry.position.end,
1050 )
1051 })
1052 .collect::<Vec<_>>()
1053 };
1054
1055 let fast = ascii_class_selector_names(selector);
1056 assert!(fast.is_some(), "fixture must stay on the fast path");
1057 if let Some(fast) = fast {
1058 assert_eq!(
1059 summarize(fast),
1060 summarize(general_class_selector_names(selector))
1061 );
1062 }
1063 }
1064
1065 #[test]
1066 fn extracts_top_level_class_names_with_byte_positions() {
1067 let selector = r#".card .title[data-x="a.b"]:is(.nested).a\.b.\31 23.카드.café"#;
1068 let names = class_selector_names(selector);
1069 let raw = names
1070 .iter()
1071 .map(|entry| entry.name.raw())
1072 .collect::<Vec<_>>();
1073
1074 assert_eq!(
1077 raw,
1078 vec!["card", "title", r"a\.b", r"\31 23", "카드", "café"]
1079 );
1080 assert_eq!(
1081 names
1082 .iter()
1083 .find(|entry| entry.name.raw() == "café")
1084 .map(|entry| entry.name.decoded().chars().count()),
1085 Some(4)
1086 );
1087 for entry in names {
1088 assert_eq!(
1089 &selector[entry.position.start..entry.position.end],
1090 entry.name.raw()
1091 );
1092 }
1093 }
1094
1095 #[test]
1096 fn distinguishes_css_name_and_ascii_word_boundaries() {
1097 assert!(is_css_name_start('카'));
1100 assert!(is_css_name_continue('é'));
1101 assert!(!is_ascii_word_continue('카'));
1102 assert!(is_ascii_word_continue('9'));
1103 assert!(is_safe_css_identifier("카드"));
1104 assert!(is_safe_css_identifier("--token"));
1105 assert!(!is_safe_css_identifier("-9token"));
1106 assert!(!is_safe_css_identifier("9token"));
1107 }
1108}