rsass/css/
string.rs

1use crate::value::Quotes;
2use std::fmt::{self, Write};
3
4/// A string in css.  May be quoted.
5#[derive(Clone, Debug, Eq, PartialOrd)]
6pub struct CssString {
7    value: String,
8    quotes: Quotes,
9}
10
11impl CssString {
12    /// Create a new `CssString`.
13    pub fn new(value: String, quotes: Quotes) -> Self {
14        Self { value, quotes }
15    }
16    /// Unquote this string.
17    pub fn unquote(self) -> String {
18        if self.quotes.is_none() {
19            self.value
20        } else {
21            let mut result = String::new();
22            let mut iter = self.value.chars().peekable();
23            while let Some(c) = iter.next() {
24                if c == '\\' {
25                    let mut val: u32 = 0;
26                    let mut got_num = false;
27                    let nextchar = loop {
28                        match iter.peek() {
29                            Some(' ') if got_num => {
30                                iter.next();
31                                break None;
32                            }
33                            Some(&c) => {
34                                if let Some(digit) = c.to_digit(16) {
35                                    val = val * 10 + digit;
36                                    got_num = true;
37                                    iter.next();
38                                } else if !got_num {
39                                    break iter.next();
40                                } else {
41                                    break None;
42                                }
43                            }
44                            _ => break None,
45                        }
46                    };
47                    if got_num {
48                        result.push(
49                            char::try_from(val)
50                                .unwrap_or(char::REPLACEMENT_CHARACTER),
51                        );
52                    }
53                    match nextchar {
54                        Some('\n') => {
55                            result.push('\\');
56                            result.push('a');
57                        }
58                        Some(c) => {
59                            result.push(c);
60                        }
61                        None => (),
62                    }
63                } else {
64                    result.push(c);
65                }
66            }
67            result
68        }
69    }
70    /// If the value is name-like, make it unquoted.
71    pub fn opt_unquote(self) -> Self {
72        let t = is_namelike(&self.value);
73        Self {
74            value: self.value,
75            quotes: if t { Quotes::None } else { self.quotes },
76        }
77    }
78    /// Quote this string
79    pub fn quote(self) -> Self {
80        let value = if self.quotes.is_none() {
81            self.value.replace('\\', "\\\\")
82        } else {
83            self.value
84        };
85        if value.contains('"') && !value.contains('\'') {
86            Self {
87                value,
88                quotes: Quotes::Single,
89            }
90        } else {
91            Self {
92                value,
93                quotes: Quotes::Double,
94            }
95        }
96    }
97    /// Adapt the kind of quotes as prefered for a css value.
98    pub fn pref_dquotes(self) -> Self {
99        let value = self.value;
100        let quotes = match self.quotes {
101            Quotes::Double
102                if value.contains('"') && !value.contains('\'') =>
103            {
104                Quotes::Single
105            }
106            Quotes::Single
107                if !value.contains('"') || value.contains('\'') =>
108            {
109                Quotes::Double
110            }
111            q => q,
112        };
113        Self { value, quotes }
114    }
115    /// Return true if this is an empty unquoted string.
116    pub fn is_null(&self) -> bool {
117        self.value.is_empty() && self.quotes.is_none()
118    }
119    pub(crate) fn is_name(&self) -> bool {
120        self.quotes == Quotes::None && is_namelike(&self.value)
121    }
122
123    /// Return true if this is a css special function call.
124    pub(crate) fn is_css_fn(&self) -> bool {
125        let value = self.value();
126        self.quotes() == Quotes::None
127            && (value.ends_with(')')
128                && (value.starts_with("calc(") || value.starts_with("var(")))
129            // The following is only relevant in relative colors
130            // but I don't see how to get that context to where its needed,
131            // and I don't see any real harm in puttin them like this.
132            || ["h", "s", "l", "r", "g", "b"].iter().any(|s| value == *s)
133    }
134    /// Return true if this is a css special function call.
135    pub(crate) fn is_css_calc(&self) -> bool {
136        let value = self.value();
137        self.quotes() == Quotes::None
138            && value.ends_with(')')
139            && (value.starts_with("calc(") || value.starts_with("clamp("))
140    }
141    /// Return true if this is a css url function call.
142    pub(crate) fn is_css_url(&self) -> bool {
143        let value = self.value();
144        self.quotes() == Quotes::None
145            && value.ends_with(')')
146            && value.starts_with("url(")
147    }
148    /// Access the string value
149    pub fn value(&self) -> &str {
150        &self.value
151    }
152    /// Take the string value, discarding quote information.
153    pub fn take_value(self) -> String {
154        self.value
155    }
156    /// Access the quotes
157    pub fn quotes(&self) -> Quotes {
158        self.quotes
159    }
160}
161
162impl<S: Into<String>> From<S> for CssString {
163    fn from(value: S) -> Self {
164        Self {
165            value: value.into(),
166            quotes: Quotes::None,
167        }
168    }
169}
170
171impl fmt::Display for CssString {
172    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
173        let q = match self.quotes {
174            Quotes::None => None,
175            Quotes::Double => Some('"'),
176            Quotes::Single => Some('\''),
177        };
178        if let Some(q) = q {
179            out.write_char(q)?;
180        }
181        for c in self.value.chars() {
182            if Some(c) == q {
183                out.write_char('\\')?;
184                out.write_char(c)?;
185            } else if is_private_use(c) {
186                write!(out, "\\{:x}", c as u32)?;
187            } else {
188                out.write_char(c)?;
189            }
190        }
191        if let Some(q) = q {
192            out.write_char(q)?;
193        };
194        Ok(())
195    }
196}
197
198impl PartialEq for CssString {
199    fn eq(&self, other: &Self) -> bool {
200        if self.quotes == other.quotes {
201            self.value == other.value
202        } else {
203            self.clone().unquote() == other.clone().unquote()
204        }
205    }
206}
207
208impl From<CssString> for crate::sass::Name {
209    fn from(s: CssString) -> Self {
210        s.value.into()
211    }
212}
213
214fn is_namelike(s: &str) -> bool {
215    let mut chars = s.chars();
216    chars
217        .next()
218        .map_or(false, |c| c.is_alphabetic() || c == '_')
219        && chars.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
220}
221
222fn is_private_use(c: char) -> bool {
223    // https://en.wikipedia.org/wiki/Private_Use_Areas
224    ('\u{E000}'..='\u{F8FF}').contains(&c)
225        || ('\u{F0000}'..='\u{FFFFD}').contains(&c)
226        || ('\u{100000}'..='\u{10FFFD}').contains(&c)
227}