1use crate::value::Quotes;
2use std::fmt::{self, Write};
3
4#[derive(Clone, Debug, Eq, PartialOrd)]
6pub struct CssString {
7 value: String,
8 quotes: Quotes,
9}
10
11impl CssString {
12 pub fn new(value: String, quotes: Quotes) -> Self {
14 Self { value, quotes }
15 }
16 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 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 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 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 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 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 || ["h", "s", "l", "r", "g", "b"].iter().any(|s| value == *s)
133 }
134 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 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 pub fn value(&self) -> &str {
150 &self.value
151 }
152 pub fn take_value(self) -> String {
154 self.value
155 }
156 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 ('\u{E000}'..='\u{F8FF}').contains(&c)
225 || ('\u{F0000}'..='\u{FFFFD}').contains(&c)
226 || ('\u{100000}'..='\u{10FFFD}').contains(&c)
227}