1 use std::cmp::Ordering;
2
3 use codemap::{Span, Spanned};
4
5 use crate::{
6 color::Color,
7 common::{Brackets, ListSeparator, Op, QuoteKind},
8 error::SassResult,
9 lexer::Lexer,
10 parse::Parser,
11 selector::Selector,
12 unit::Unit,
13 utils::hex_char_for,
14 {Cow, Token},
15 };
16
17 use css_function::is_special_function;
18 pub(crate) use map::SassMap;
19 pub(crate) use number::Number;
20 pub(crate) use sass_function::SassFunction;
21
22 pub(crate) mod css_function;
23 mod map;
24 mod number;
25 mod sass_function;
26
27 #[derive(Debug, Clone)]
28 pub(crate) enum Value {
29 Important,
30 True,
31 False,
32 Null,
33 /// A `None` value for `Number` indicates a `NaN` value
34 Dimension(Option<Number>, Unit, bool),
35 List(Vec<Value>, ListSeparator, Brackets),
36 Color(Box<Color>),
37 String(String, QuoteKind),
38 Map(SassMap),
39 ArgList(Vec<Spanned<Value>>),
40 /// Returned by `get-function()`
41 FunctionRef(SassFunction),
42 }
43
44 impl PartialEq for Value {
eq(&self, other: &Self) -> bool45 fn eq(&self, other: &Self) -> bool {
46 match self {
47 Value::String(s1, ..) => match other {
48 Value::String(s2, ..) => s1 == s2,
49 _ => false,
50 },
51 Value::Dimension(Some(n), unit, _) => match other {
52 Value::Dimension(Some(n2), unit2, _) => {
53 if !unit.comparable(unit2) {
54 false
55 } else if unit == unit2 {
56 n == n2
57 } else if unit == &Unit::None || unit2 == &Unit::None {
58 false
59 } else {
60 n == &n2.clone().convert(unit2, unit)
61 }
62 }
63 _ => false,
64 },
65 Value::Dimension(None, ..) => false,
66 Value::List(list1, sep1, brackets1) => match other {
67 Value::List(list2, sep2, brackets2) => {
68 if sep1 != sep2 || brackets1 != brackets2 || list1.len() != list2.len() {
69 false
70 } else {
71 for (a, b) in list1.iter().zip(list2) {
72 if a != b {
73 return false;
74 }
75 }
76 true
77 }
78 }
79 _ => false,
80 },
81 Value::Null => matches!(other, Value::Null),
82 Value::True => matches!(other, Value::True),
83 Value::False => matches!(other, Value::False),
84 Value::Important => matches!(other, Value::Important),
85 Value::FunctionRef(fn1) => {
86 if let Value::FunctionRef(fn2) = other {
87 fn1 == fn2
88 } else {
89 false
90 }
91 }
92 Value::Map(map1) => {
93 if let Value::Map(map2) = other {
94 map1 == map2
95 } else {
96 false
97 }
98 }
99 Value::Color(color1) => {
100 if let Value::Color(color2) = other {
101 color1 == color2
102 } else {
103 false
104 }
105 }
106 Value::ArgList(list1) => match other {
107 Value::ArgList(list2) => list1 == list2,
108 Value::List(list2, ListSeparator::Comma, ..) => {
109 if list1.len() != list2.len() {
110 return false;
111 }
112
113 for (el1, el2) in list1.iter().zip(list2) {
114 if &el1.node != el2 {
115 return false;
116 }
117 }
118
119 true
120 }
121 _ => false,
122 },
123 }
124 }
125 }
126
127 impl Eq for Value {}
128
visit_quoted_string(buf: &mut String, force_double_quote: bool, string: &str)129 fn visit_quoted_string(buf: &mut String, force_double_quote: bool, string: &str) {
130 let mut has_single_quote = false;
131 let mut has_double_quote = false;
132
133 let mut buffer = String::new();
134
135 if force_double_quote {
136 buffer.push('"');
137 }
138 let mut iter = string.chars().peekable();
139 while let Some(c) = iter.next() {
140 match c {
141 '\'' => {
142 if force_double_quote {
143 buffer.push('\'');
144 } else if has_double_quote {
145 return visit_quoted_string(buf, true, string);
146 } else {
147 has_single_quote = true;
148 buffer.push('\'');
149 }
150 }
151 '"' => {
152 if force_double_quote {
153 buffer.push('\\');
154 buffer.push('"');
155 } else if has_single_quote {
156 return visit_quoted_string(buf, true, string);
157 } else {
158 has_double_quote = true;
159 buffer.push('"');
160 }
161 }
162 '\x00'..='\x08' | '\x0A'..='\x1F' => {
163 buffer.push('\\');
164 if c as u32 > 0xF {
165 buffer.push(hex_char_for(c as u32 >> 4));
166 }
167 buffer.push(hex_char_for(c as u32 & 0xF));
168
169 let next = match iter.peek() {
170 Some(v) => v,
171 None => break,
172 };
173
174 if next.is_ascii_hexdigit() || next == &' ' || next == &'\t' {
175 buffer.push(' ');
176 }
177 }
178 '\\' => {
179 buffer.push('\\');
180 buffer.push('\\');
181 }
182 _ => buffer.push(c),
183 }
184 }
185
186 if force_double_quote {
187 buffer.push('"');
188 } else {
189 let quote = if has_double_quote { '\'' } else { '"' };
190 buffer = format!("{}{}{}", quote, buffer, quote);
191 }
192 buf.push_str(&buffer);
193 }
194
195 impl Value {
is_null(&self) -> bool196 pub fn is_null(&self) -> bool {
197 match self {
198 Value::Null => true,
199 Value::String(i, QuoteKind::None) if i.is_empty() => true,
200 Value::List(v, _, Brackets::Bracketed) if v.is_empty() => false,
201 Value::List(v, ..) => v.iter().map(Value::is_null).all(|f| f),
202 Value::ArgList(v, ..) if v.is_empty() => false,
203 Value::ArgList(v, ..) => v.iter().map(|v| v.node.is_null()).all(|f| f),
204 _ => false,
205 }
206 }
207
to_css_string(&self, span: Span, is_compressed: bool) -> SassResult<Cow<'static, str>>208 pub fn to_css_string(&self, span: Span, is_compressed: bool) -> SassResult<Cow<'static, str>> {
209 Ok(match self {
210 Value::Important => Cow::const_str("!important"),
211 Value::Dimension(num, unit, _) => match unit {
212 Unit::Mul(..) | Unit::Div(..) => {
213 if let Some(num) = num {
214 return Err((
215 format!(
216 "{}{} isn't a valid CSS value.",
217 num.to_string(is_compressed),
218 unit
219 ),
220 span,
221 )
222 .into());
223 }
224
225 return Err((format!("NaN{} isn't a valid CSS value.", unit), span).into());
226 }
227 _ => {
228 if let Some(num) = num {
229 Cow::owned(format!("{}{}", num.to_string(is_compressed), unit))
230 } else {
231 Cow::owned(format!("NaN{}", unit))
232 }
233 }
234 },
235 Value::Map(..) | Value::FunctionRef(..) => {
236 return Err((
237 format!("{} isn't a valid CSS value.", self.inspect(span)?),
238 span,
239 )
240 .into())
241 }
242 Value::List(vals, sep, brackets) => match brackets {
243 Brackets::None => Cow::owned(
244 vals.iter()
245 .filter(|x| !x.is_null())
246 .map(|x| x.to_css_string(span, is_compressed))
247 .collect::<SassResult<Vec<Cow<'static, str>>>>()?
248 .join(if is_compressed {
249 sep.as_compressed_str()
250 } else {
251 sep.as_str()
252 }),
253 ),
254 Brackets::Bracketed => Cow::owned(format!(
255 "[{}]",
256 vals.iter()
257 .filter(|x| !x.is_null())
258 .map(|x| x.to_css_string(span, is_compressed))
259 .collect::<SassResult<Vec<Cow<'static, str>>>>()?
260 .join(if is_compressed {
261 sep.as_compressed_str()
262 } else {
263 sep.as_str()
264 }),
265 )),
266 },
267 Value::Color(c) => Cow::owned(c.to_string()),
268 Value::String(string, QuoteKind::None) => {
269 let mut after_newline = false;
270 let mut buf = String::with_capacity(string.len());
271 for c in string.chars() {
272 match c {
273 '\n' => {
274 buf.push(' ');
275 after_newline = true;
276 }
277 ' ' => {
278 if !after_newline {
279 buf.push(' ');
280 }
281 }
282 _ => {
283 buf.push(c);
284 after_newline = false;
285 }
286 }
287 }
288 Cow::owned(buf)
289 }
290 Value::String(string, QuoteKind::Quoted) => {
291 let mut buf = String::with_capacity(string.len());
292 visit_quoted_string(&mut buf, false, string);
293 Cow::owned(buf)
294 }
295 Value::True => Cow::const_str("true"),
296 Value::False => Cow::const_str("false"),
297 Value::Null => Cow::const_str(""),
298 Value::ArgList(args) if args.is_empty() => {
299 return Err(("() isn't a valid CSS value.", span).into());
300 }
301 Value::ArgList(args) => Cow::owned(
302 args.iter()
303 .filter(|x| !x.is_null())
304 .map(|a| a.node.to_css_string(span, is_compressed))
305 .collect::<SassResult<Vec<Cow<'static, str>>>>()?
306 .join(if is_compressed {
307 ListSeparator::Comma.as_compressed_str()
308 } else {
309 ListSeparator::Comma.as_str()
310 }),
311 ),
312 })
313 }
314
is_true(&self) -> bool315 pub fn is_true(&self) -> bool {
316 !matches!(self, Value::Null | Value::False)
317 }
318
unquote(self) -> Self319 pub fn unquote(self) -> Self {
320 match self {
321 Value::String(s1, _) => Value::String(s1, QuoteKind::None),
322 Value::List(v, sep, bracket) => {
323 Value::List(v.into_iter().map(Value::unquote).collect(), sep, bracket)
324 }
325 v => v,
326 }
327 }
328
span(self, span: Span) -> Spanned<Self>329 pub const fn span(self, span: Span) -> Spanned<Self> {
330 Spanned { node: self, span }
331 }
332
kind(&self) -> &'static str333 pub fn kind(&self) -> &'static str {
334 match self {
335 Value::Color(..) => "color",
336 Value::String(..) | Value::Important => "string",
337 Value::Dimension(..) => "number",
338 Value::List(..) => "list",
339 Value::FunctionRef(..) => "function",
340 Value::ArgList(..) => "arglist",
341 Value::True | Value::False => "bool",
342 Value::Null => "null",
343 Value::Map(..) => "map",
344 }
345 }
346
is_color(&self) -> bool347 pub fn is_color(&self) -> bool {
348 matches!(self, Value::Color(..))
349 }
350
is_special_function(&self) -> bool351 pub fn is_special_function(&self) -> bool {
352 match self {
353 Value::String(s, QuoteKind::None) => is_special_function(s),
354 _ => false,
355 }
356 }
357
bool(b: bool) -> Self358 pub fn bool(b: bool) -> Self {
359 if b {
360 Value::True
361 } else {
362 Value::False
363 }
364 }
365
cmp(&self, other: &Self, span: Span, op: Op) -> SassResult<Ordering>366 pub fn cmp(&self, other: &Self, span: Span, op: Op) -> SassResult<Ordering> {
367 Ok(match self {
368 Value::Dimension(None, ..) => todo!(),
369 Value::Dimension(Some(num), unit, _) => match &other {
370 Value::Dimension(None, ..) => todo!(),
371 Value::Dimension(Some(num2), unit2, _) => {
372 if !unit.comparable(unit2) {
373 return Err(
374 (format!("Incompatible units {} and {}.", unit2, unit), span).into(),
375 );
376 }
377 if unit == unit2 || unit == &Unit::None || unit2 == &Unit::None {
378 num.cmp(num2)
379 } else {
380 num.cmp(&num2.clone().convert(unit2, unit))
381 }
382 }
383 _ => {
384 return Err((
385 format!(
386 "Undefined operation \"{} {} {}\".",
387 self.inspect(span)?,
388 op,
389 other.inspect(span)?
390 ),
391 span,
392 )
393 .into())
394 }
395 },
396 _ => {
397 return Err((
398 format!(
399 "Undefined operation \"{} {} {}\".",
400 self.inspect(span)?,
401 op,
402 other.inspect(span)?
403 ),
404 span,
405 )
406 .into());
407 }
408 })
409 }
410
not_equals(&self, other: &Self) -> bool411 pub fn not_equals(&self, other: &Self) -> bool {
412 match self {
413 Value::String(s1, ..) => match other {
414 Value::String(s2, ..) => s1 != s2,
415 _ => true,
416 },
417 Value::Dimension(Some(n), unit, _) => match other {
418 Value::Dimension(Some(n2), unit2, _) => {
419 if !unit.comparable(unit2) {
420 true
421 } else if unit == unit2 {
422 n != n2
423 } else if unit == &Unit::None || unit2 == &Unit::None {
424 true
425 } else {
426 n != &n2.clone().convert(unit2, unit)
427 }
428 }
429 _ => true,
430 },
431 Value::List(list1, sep1, brackets1) => match other {
432 Value::List(list2, sep2, brackets2) => {
433 if sep1 != sep2 || brackets1 != brackets2 || list1.len() != list2.len() {
434 true
435 } else {
436 for (a, b) in list1.iter().zip(list2) {
437 if a.not_equals(b) {
438 return true;
439 }
440 }
441 false
442 }
443 }
444 _ => true,
445 },
446 s => s != other,
447 }
448 }
449
450 // TODO:
451 // https://github.com/sass/dart-sass/blob/d4adea7569832f10e3a26d0e420ae51640740cfb/lib/src/ast/sass/expression/list.dart#L39
inspect(&self, span: Span) -> SassResult<Cow<'static, str>>452 pub fn inspect(&self, span: Span) -> SassResult<Cow<'static, str>> {
453 Ok(match self {
454 Value::List(v, _, brackets) if v.is_empty() => match brackets {
455 Brackets::None => Cow::const_str("()"),
456 Brackets::Bracketed => Cow::const_str("[]"),
457 },
458 Value::List(v, sep, brackets) if v.len() == 1 => match brackets {
459 Brackets::None => match sep {
460 ListSeparator::Space => v[0].inspect(span)?,
461 ListSeparator::Comma => Cow::owned(format!("({},)", v[0].inspect(span)?)),
462 },
463 Brackets::Bracketed => match sep {
464 ListSeparator::Space => Cow::owned(format!("[{}]", v[0].inspect(span)?)),
465 ListSeparator::Comma => Cow::owned(format!("[{},]", v[0].inspect(span)?)),
466 },
467 },
468 Value::List(vals, sep, brackets) => Cow::owned(match brackets {
469 Brackets::None => vals
470 .iter()
471 .map(|x| x.inspect(span))
472 .collect::<SassResult<Vec<Cow<'static, str>>>>()?
473 .join(sep.as_str()),
474 Brackets::Bracketed => format!(
475 "[{}]",
476 vals.iter()
477 .map(|x| x.inspect(span))
478 .collect::<SassResult<Vec<Cow<'static, str>>>>()?
479 .join(sep.as_str()),
480 ),
481 }),
482 Value::FunctionRef(f) => Cow::owned(format!("get-function(\"{}\")", f.name())),
483 Value::Null => Cow::const_str("null"),
484 Value::Map(map) => Cow::owned(format!(
485 "({})",
486 map.iter()
487 .map(|(k, v)| Ok(format!("{}: {}", k.inspect(span)?, v.inspect(span)?)))
488 .collect::<SassResult<Vec<String>>>()?
489 .join(", ")
490 )),
491 Value::Dimension(Some(num), unit, _) => {
492 Cow::owned(format!("{}{}", num.inspect(), unit))
493 }
494 Value::Dimension(None, unit, ..) => Cow::owned(format!("NaN{}", unit)),
495 Value::ArgList(args) if args.is_empty() => Cow::const_str("()"),
496 Value::ArgList(args) if args.len() == 1 => Cow::owned(format!(
497 "({},)",
498 args.iter()
499 .filter(|x| !x.is_null())
500 .map(|a| a.node.inspect(span))
501 .collect::<SassResult<Vec<Cow<'static, str>>>>()?
502 .join(", "),
503 )),
504 Value::ArgList(args) => Cow::owned(
505 args.iter()
506 .filter(|x| !x.is_null())
507 .map(|a| a.node.inspect(span))
508 .collect::<SassResult<Vec<Cow<'static, str>>>>()?
509 .join(", "),
510 ),
511 Value::Important
512 | Value::True
513 | Value::False
514 | Value::Color(..)
515 | Value::String(..) => self.to_css_string(span, false)?,
516 })
517 }
518
as_list(self) -> Vec<Value>519 pub fn as_list(self) -> Vec<Value> {
520 match self {
521 Value::List(v, ..) => v,
522 Value::Map(m) => m.as_list(),
523 Value::ArgList(v) => v.into_iter().map(|val| val.node).collect(),
524 v => vec![v],
525 }
526 }
527
528 /// Parses `self` as a selector list, in the same manner as the
529 /// `selector-parse()` function.
530 ///
531 /// Returns a `SassError` if `self` isn't a type that can be parsed as a
532 /// selector, or if parsing fails. If `allow_parent` is `true`, this allows
533 /// parent selectors. Otherwise, they're considered parse errors.
534 ///
535 /// `name` is the argument name. It's used for error reporting.
to_selector( self, parser: &mut Parser, name: &str, allows_parent: bool, ) -> SassResult<Selector>536 pub fn to_selector(
537 self,
538 parser: &mut Parser,
539 name: &str,
540 allows_parent: bool,
541 ) -> SassResult<Selector> {
542 let string = match self.clone().selector_string(parser.span_before)? {
543 Some(v) => v,
544 None => return Err((format!("${}: {} is not a valid selector: it must be a string, a list of strings, or a list of lists of strings.", name, self.inspect(parser.span_before)?), parser.span_before).into()),
545 };
546 Ok(Parser {
547 toks: &mut Lexer::new(
548 string
549 .chars()
550 .map(|c| Token::new(parser.span_before, c))
551 .collect::<Vec<Token>>(),
552 ),
553 map: parser.map,
554 path: parser.path,
555 scopes: parser.scopes,
556 global_scope: parser.global_scope,
557 super_selectors: parser.super_selectors,
558 span_before: parser.span_before,
559 content: parser.content,
560 flags: parser.flags,
561 at_root: parser.at_root,
562 at_root_has_selector: parser.at_root_has_selector,
563 extender: parser.extender,
564 content_scopes: parser.content_scopes,
565 options: parser.options,
566 modules: parser.modules,
567 module_config: parser.module_config,
568 }
569 .parse_selector(allows_parent, true, String::new())?
570 .0)
571 }
572
selector_string(self, span: Span) -> SassResult<Option<String>>573 fn selector_string(self, span: Span) -> SassResult<Option<String>> {
574 Ok(Some(match self {
575 Value::String(text, ..) => text,
576 Value::List(list, sep, ..) if !list.is_empty() => {
577 let mut result = Vec::new();
578 match sep {
579 ListSeparator::Comma => {
580 for complex in list {
581 if let Value::String(text, ..) = complex {
582 result.push(text);
583 } else if let Value::List(_, ListSeparator::Space, ..) = complex {
584 result.push(match complex.selector_string(span)? {
585 Some(v) => v,
586 None => return Ok(None),
587 });
588 } else {
589 return Ok(None);
590 }
591 }
592 }
593 ListSeparator::Space => {
594 for compound in list {
595 if let Value::String(text, ..) = compound {
596 result.push(text);
597 } else {
598 return Ok(None);
599 }
600 }
601 }
602 }
603
604 result.join(sep.as_str())
605 }
606 _ => return Ok(None),
607 }))
608 }
609
is_quoted_string(&self) -> bool610 pub fn is_quoted_string(&self) -> bool {
611 matches!(self, Value::String(_, QuoteKind::Quoted))
612 }
613 }
614