Skip to main content

omena_syntax/
layer.rs

1//! Canonical identity for CSS cascade-layer paths.
2
3use crate::ident::decode_css_identifier_escapes;
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
6struct CanonicalLayerIdentifierKeySealV0(());
7
8/// One escape-decoded layer identifier. The private seal prevents callers from
9/// treating authored spelling as the identity key.
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct CanonicalLayerIdentifierKeyV0(String, CanonicalLayerIdentifierKeySealV0);
12
13impl CanonicalLayerIdentifierKeyV0 {
14    pub fn from_authored(authored: &str) -> Option<Self> {
15        let decoded = decode_css_identifier_escapes(authored).into_owned();
16        (!decoded.is_empty()).then_some(Self(decoded, CanonicalLayerIdentifierKeySealV0(())))
17    }
18
19    pub fn as_str(&self) -> &str {
20        self.0.as_str()
21    }
22}
23
24impl serde::Serialize for CanonicalLayerIdentifierKeyV0 {
25    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
26    where
27        S: serde::Serializer,
28    {
29        serializer.serialize_str(self.as_str())
30    }
31}
32
33/// A parser-issued path of decoded CSS identifiers.
34#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub struct LayerPathV0 {
36    segments: Vec<CanonicalLayerIdentifierKeyV0>,
37}
38
39impl LayerPathV0 {
40    pub fn from_authored_segments<'a>(segments: impl IntoIterator<Item = &'a str>) -> Option<Self> {
41        let segments = segments
42            .into_iter()
43            .map(CanonicalLayerIdentifierKeyV0::from_authored)
44            .collect::<Option<Vec<_>>>()?;
45        (!segments.is_empty()).then_some(Self { segments })
46    }
47
48    pub fn segments(&self) -> &[CanonicalLayerIdentifierKeyV0] {
49        self.segments.as_slice()
50    }
51
52    pub fn nesting_depth(&self) -> usize {
53        self.segments.len().saturating_sub(1)
54    }
55
56    pub fn canonical_name(&self) -> String {
57        self.segments
58            .iter()
59            .map(|segment| canonical_css_identifier(segment.as_str()))
60            .collect::<Vec<_>>()
61            .join(".")
62    }
63
64    pub fn prefix(&self, segment_count: usize) -> Option<Self> {
65        (segment_count > 0 && segment_count <= self.segments.len()).then(|| Self {
66            segments: self.segments[..segment_count].to_vec(),
67        })
68    }
69
70    pub fn parent(&self) -> Option<Self> {
71        self.prefix(self.segments.len().checked_sub(1)?)
72    }
73
74    pub fn local_name(&self) -> &str {
75        self.segments
76            .last()
77            .map(CanonicalLayerIdentifierKeyV0::as_str)
78            .unwrap_or_default()
79    }
80
81    pub fn joined(&self, local: &Self) -> Self {
82        let mut segments = self.segments.clone();
83        segments.extend(local.segments.iter().cloned());
84        Self { segments }
85    }
86}
87
88fn canonical_css_identifier(identifier: &str) -> String {
89    let chars = identifier.chars().collect::<Vec<_>>();
90    let mut canonical = String::with_capacity(identifier.len());
91    for (index, character) in chars.iter().copied().enumerate() {
92        let ascii_name_character = character.is_ascii_alphabetic()
93            || character == '_'
94            || character == '-'
95            || (character.is_ascii_digit()
96                && index > 0
97                && !(index == 1 && chars.first() == Some(&'-')));
98        if ascii_name_character || !character.is_ascii() {
99            canonical.push(character);
100        } else {
101            canonical.push('\\');
102            canonical.push_str(format!("{:06x}", u32::from(character)).as_str());
103        }
104    }
105    canonical
106}