1use std::io::Write;
2
3use codemap::{CodeMap, Span};
4
5use crate::{
6 ast::{CssStmt, MediaQuery, Style, SupportsRule},
7 color::{Color, ColorFormat, NAMED_COLORS},
8 common::{BinaryOp, Brackets, ListSeparator, QuoteKind},
9 error::SassResult,
10 selector::{
11 Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, Pseudo,
12 SelectorList, SimpleSelector,
13 },
14 utils::hex_char_for,
15 value::{
16 fuzzy_equals, ArgList, CalculationArg, CalculationName, SassCalculation, SassFunction,
17 SassMap, SassNumber, Value,
18 },
19 Options,
20};
21
22pub(crate) fn serialize_selector_list(
23 list: &SelectorList,
24 options: &Options,
25 span: Span,
26) -> String {
27 let map = CodeMap::new();
28 let mut serializer = Serializer::new(options, &map, false, span);
29
30 serializer.write_selector_list(list);
31
32 serializer.finish_for_expr()
33}
34
35pub(crate) fn serialize_calculation_arg(
36 arg: &CalculationArg,
37 options: &Options,
38 span: Span,
39) -> SassResult<String> {
40 let map = CodeMap::new();
41 let mut serializer = Serializer::new(options, &map, false, span);
42
43 serializer.write_calculation_arg(arg)?;
44
45 Ok(serializer.finish_for_expr())
46}
47
48pub(crate) fn serialize_number(
49 number: &SassNumber,
50 options: &Options,
51 span: Span,
52) -> SassResult<String> {
53 let map = CodeMap::new();
54 let mut serializer = Serializer::new(options, &map, false, span);
55
56 serializer.visit_number(number)?;
57
58 Ok(serializer.finish_for_expr())
59}
60
61pub(crate) fn serialize_value(val: &Value, options: &Options, span: Span) -> SassResult<String> {
62 let map = CodeMap::new();
63 let mut serializer = Serializer::new(options, &map, false, span);
64
65 serializer.visit_value(val, span)?;
66
67 Ok(serializer.finish_for_expr())
68}
69
70pub(crate) fn inspect_value(val: &Value, options: &Options, span: Span) -> SassResult<String> {
71 let map = CodeMap::new();
72 let mut serializer = Serializer::new(options, &map, true, span);
73
74 serializer.visit_value(val, span)?;
75
76 Ok(serializer.finish_for_expr())
77}
78
79pub(crate) fn inspect_float(number: f64, options: &Options, span: Span) -> String {
80 let map = CodeMap::new();
81 let mut serializer = Serializer::new(options, &map, true, span);
82
83 serializer.write_float(number);
84
85 serializer.finish_for_expr()
86}
87
88pub(crate) fn inspect_map(map: &SassMap, options: &Options, span: Span) -> SassResult<String> {
89 let code_map = CodeMap::new();
90 let mut serializer = Serializer::new(options, &code_map, true, span);
91
92 serializer.visit_map(map, span)?;
93
94 Ok(serializer.finish_for_expr())
95}
96
97pub(crate) fn inspect_function_ref(
98 func: &SassFunction,
99 options: &Options,
100 span: Span,
101) -> SassResult<String> {
102 let code_map = CodeMap::new();
103 let mut serializer = Serializer::new(options, &code_map, true, span);
104
105 serializer.visit_function_ref(func, span)?;
106
107 Ok(serializer.finish_for_expr())
108}
109
110pub(crate) fn inspect_number(
111 number: &SassNumber,
112 options: &Options,
113 span: Span,
114) -> SassResult<String> {
115 let map = CodeMap::new();
116 let mut serializer = Serializer::new(options, &map, true, span);
117
118 serializer.visit_number(number)?;
119
120 Ok(serializer.finish_for_expr())
121}
122
123pub(crate) struct Serializer<'a> {
124 indentation: usize,
125 options: &'a Options<'a>,
126 inspect: bool,
127 indent_width: usize,
128 _quote: bool,
130 buffer: Vec<u8>,
131 map: &'a CodeMap,
132 span: Span,
133}
134
135impl<'a> Serializer<'a> {
136 pub fn new(options: &'a Options<'a>, map: &'a CodeMap, inspect: bool, span: Span) -> Self {
137 Self {
138 inspect,
139 _quote: true,
140 indentation: 0,
141 indent_width: 2,
142 options,
143 buffer: Vec::new(),
144 map,
145 span,
146 }
147 }
148
149 fn omit_spaces_around_complex_component(&self, component: &ComplexSelectorComponent) -> bool {
150 self.options.is_compressed()
151 && matches!(component, ComplexSelectorComponent::Combinator(..))
152 }
153
154 fn write_pseudo_selector(&mut self, pseudo: &Pseudo) {
155 if let Some(sel) = &pseudo.selector {
156 if pseudo.name == "not" && sel.is_invisible() {
157 return;
158 }
159 }
160
161 self.buffer.push(b':');
162
163 if !pseudo.is_syntactic_class {
164 self.buffer.push(b':');
165 }
166
167 self.buffer.extend_from_slice(pseudo.name.as_bytes());
168
169 if pseudo.argument.is_none() && pseudo.selector.is_none() {
170 return;
171 }
172
173 self.buffer.push(b'(');
174 if let Some(arg) = &pseudo.argument {
175 self.buffer.extend_from_slice(arg.as_bytes());
176 if pseudo.selector.is_some() {
177 self.buffer.push(b' ');
178 }
179 }
180
181 if let Some(sel) = &pseudo.selector {
182 self.write_selector_list(sel);
183 }
184
185 self.buffer.push(b')');
186 }
187
188 fn write_namespace(&mut self, namespace: &Namespace) {
189 match namespace {
190 Namespace::Empty => self.buffer.push(b'|'),
191 Namespace::Asterisk => self.buffer.extend_from_slice(b"*|"),
192 Namespace::Other(namespace) => {
193 self.buffer.extend_from_slice(namespace.as_bytes());
194 self.buffer.push(b'|');
195 }
196 Namespace::None => {}
197 }
198 }
199
200 fn write_simple_selector(&mut self, simple: &SimpleSelector) {
201 match simple {
202 SimpleSelector::Id(name) => {
203 self.buffer.push(b'#');
204 self.buffer.extend_from_slice(name.as_bytes());
205 }
206 SimpleSelector::Class(name) => {
207 self.buffer.push(b'.');
208 self.buffer.extend_from_slice(name.as_bytes());
209 }
210 SimpleSelector::Placeholder(name) => {
211 self.buffer.push(b'%');
212 self.buffer.extend_from_slice(name.as_bytes());
213 }
214 SimpleSelector::Universal(namespace) => {
215 self.write_namespace(namespace);
216 self.buffer.push(b'*');
217 }
218 SimpleSelector::Pseudo(pseudo) => self.write_pseudo_selector(pseudo),
219 SimpleSelector::Type(name) => {
220 self.write_namespace(&name.namespace);
221 self.buffer.extend_from_slice(name.ident.as_bytes());
222 }
223 SimpleSelector::Attribute(attr) => write!(&mut self.buffer, "{}", attr).unwrap(),
224 SimpleSelector::Parent(..) => unreachable!("It should not be possible to format `&`."),
225 }
226 }
227
228 fn write_compound_selector(&mut self, compound: &CompoundSelector) {
229 let mut did_write = false;
230 for simple in &compound.components {
231 if did_write {
232 self.write_simple_selector(simple);
233 } else {
234 let len = self.buffer.len();
235 self.write_simple_selector(simple);
236 if self.buffer.len() != len {
237 did_write = true;
238 }
239 }
240 }
241
242 if !did_write {
246 self.buffer.push(b'*');
247 }
248 }
249
250 fn write_complex_selector_component(&mut self, component: &ComplexSelectorComponent) {
251 match component {
252 ComplexSelectorComponent::Combinator(Combinator::NextSibling) => self.buffer.push(b'+'),
253 ComplexSelectorComponent::Combinator(Combinator::Child) => self.buffer.push(b'>'),
254 ComplexSelectorComponent::Combinator(Combinator::FollowingSibling) => {
255 self.buffer.push(b'~')
256 }
257 ComplexSelectorComponent::Compound(compound) => self.write_compound_selector(compound),
258 }
259 }
260
261 fn write_complex_selector(&mut self, complex: &ComplexSelector) {
262 let mut last_component = None;
263
264 for component in &complex.components {
265 if let Some(c) = last_component {
266 if !self.omit_spaces_around_complex_component(c)
267 && !self.omit_spaces_around_complex_component(component)
268 {
269 self.buffer.push(b' ');
270 }
271 }
272 self.write_complex_selector_component(component);
273 last_component = Some(component);
274 }
275 }
276
277 fn write_selector_list(&mut self, list: &SelectorList) {
278 let complexes = list.components.iter().filter(|c| !c.is_invisible());
279
280 let mut first = true;
281
282 for complex in complexes {
283 if first {
284 first = false;
285 } else {
286 self.buffer.push(b',');
287 if complex.line_break {
288 self.write_newline();
289 } else {
290 self.write_optional_space();
291 }
292 }
293 self.write_complex_selector(complex);
294 }
295 }
296
297 fn write_newline(&mut self) {
298 if !self.options.is_compressed() {
299 self.buffer.push(b'\n');
300 }
301 }
302
303 fn write_comma_separator(&mut self) {
304 self.buffer.push(b',');
305 self.write_optional_space();
306 }
307
308 fn write_calculation_name(&mut self, name: CalculationName) {
309 match name {
310 CalculationName::Calc => self.buffer.extend_from_slice(b"calc"),
311 CalculationName::Min => self.buffer.extend_from_slice(b"min"),
312 CalculationName::Max => self.buffer.extend_from_slice(b"max"),
313 CalculationName::Clamp => self.buffer.extend_from_slice(b"clamp"),
314 }
315 }
316
317 fn visit_calculation(&mut self, calculation: &SassCalculation) -> SassResult<()> {
318 self.write_calculation_name(calculation.name);
319 self.buffer.push(b'(');
320
321 if let Some((last, slice)) = calculation.args.split_last() {
322 for arg in slice {
323 self.write_calculation_arg(arg)?;
324 self.write_comma_separator();
325 }
326
327 self.write_calculation_arg(last)?;
328 }
329
330 self.buffer.push(b')');
331
332 Ok(())
333 }
334
335 fn write_calculation_arg(&mut self, arg: &CalculationArg) -> SassResult<()> {
336 match arg {
337 CalculationArg::Number(num) => self.visit_number(num)?,
338 CalculationArg::Calculation(calc) => {
339 self.visit_calculation(calc)?;
340 }
341 CalculationArg::String(s) | CalculationArg::Interpolation(s) => {
342 self.buffer.extend_from_slice(s.as_bytes());
343 }
344 CalculationArg::Operation { lhs, op, rhs } => {
345 let paren_left = match &**lhs {
346 CalculationArg::Interpolation(..) => true,
347 CalculationArg::Operation { op: op2, .. } => op2.precedence() < op.precedence(),
348 _ => false,
349 };
350
351 if paren_left {
352 self.buffer.push(b'(');
353 }
354
355 self.write_calculation_arg(lhs)?;
356
357 if paren_left {
358 self.buffer.push(b')');
359 }
360
361 let operator_whitespace =
362 !self.options.is_compressed() || matches!(op, BinaryOp::Plus | BinaryOp::Minus);
363
364 if operator_whitespace {
365 self.buffer.push(b' ');
366 }
367
368 self.buffer.extend_from_slice(op.to_string().as_bytes());
370
371 if operator_whitespace {
372 self.buffer.push(b' ');
373 }
374
375 let paren_right = match &**rhs {
376 CalculationArg::Interpolation(..) => true,
377 CalculationArg::Operation { op: op2, .. } => {
378 CalculationArg::parenthesize_calculation_rhs(*op, *op2)
379 }
380 _ => false,
381 };
382
383 if paren_right {
384 self.buffer.push(b'(');
385 }
386
387 self.write_calculation_arg(rhs)?;
388
389 if paren_right {
390 self.buffer.push(b')');
391 }
392 }
393 }
394
395 Ok(())
396 }
397
398 fn write_rgb(&mut self, color: &Color) {
399 let is_opaque = fuzzy_equals(color.alpha().0, 1.0);
400
401 if is_opaque {
402 self.buffer.extend_from_slice(b"rgb(");
403 } else {
404 self.buffer.extend_from_slice(b"rgba(");
405 }
406
407 self.write_float(color.red().0);
408 self.buffer.extend_from_slice(b",");
409 self.write_optional_space();
410 self.write_float(color.green().0);
411 self.buffer.extend_from_slice(b",");
412 self.write_optional_space();
413 self.write_float(color.blue().0);
414
415 if !is_opaque {
416 self.buffer.extend_from_slice(b",");
417 self.write_optional_space();
418 self.write_float(color.alpha().0);
419 }
420
421 self.buffer.push(b')');
422 }
423
424 fn write_hsl(&mut self, color: &Color) {
425 let is_opaque = fuzzy_equals(color.alpha().0, 1.0);
426
427 if is_opaque {
428 self.buffer.extend_from_slice(b"hsl(");
429 } else {
430 self.buffer.extend_from_slice(b"hsla(");
431 }
432
433 self.write_float(color.hue().0);
434 self.buffer.extend_from_slice(b"deg, ");
435 self.write_float(color.saturation().0);
436 self.buffer.extend_from_slice(b"%, ");
437 self.write_float(color.lightness().0);
438 self.buffer.extend_from_slice(b"%");
439
440 if !is_opaque {
441 self.buffer.extend_from_slice(b", ");
442 self.write_float(color.alpha().0);
443 }
444
445 self.buffer.push(b')');
446 }
447
448 fn write_hex_component(&mut self, channel: u32) {
449 debug_assert!(channel < 256);
450
451 self.buffer.push(hex_char_for(channel >> 4) as u8);
452 self.buffer.push(hex_char_for(channel & 0xF) as u8);
453 }
454
455 fn is_symmetrical_hex(channel: u32) -> bool {
456 channel & 0xF == channel >> 4
457 }
458
459 fn can_use_short_hex(color: &Color) -> bool {
460 Self::is_symmetrical_hex(color.red().0.round() as u32)
461 && Self::is_symmetrical_hex(color.green().0.round() as u32)
462 && Self::is_symmetrical_hex(color.blue().0.round() as u32)
463 }
464
465 pub fn visit_color(&mut self, color: &Color) {
466 let red = color.red().0.round() as u8;
467 let green = color.green().0.round() as u8;
468 let blue = color.blue().0.round() as u8;
469
470 let name = if fuzzy_equals(color.alpha().0, 1.0) {
471 NAMED_COLORS.get_by_rgba([red, green, blue])
472 } else {
473 None
474 };
475
476 #[allow(clippy::unnecessary_unwrap)]
477 if self.options.is_compressed() {
478 if fuzzy_equals(color.alpha().0, 1.0) {
479 let hex_length = if Self::can_use_short_hex(color) { 4 } else { 7 };
480 if name.is_some() && name.unwrap().len() <= hex_length {
481 self.buffer.extend_from_slice(name.unwrap().as_bytes());
482 } else if Self::can_use_short_hex(color) {
483 self.buffer.push(b'#');
484 self.buffer.push(hex_char_for(red as u32 & 0xF) as u8);
485 self.buffer.push(hex_char_for(green as u32 & 0xF) as u8);
486 self.buffer.push(hex_char_for(blue as u32 & 0xF) as u8);
487 } else {
488 self.buffer.push(b'#');
489 self.write_hex_component(red as u32);
490 self.write_hex_component(green as u32);
491 self.write_hex_component(blue as u32);
492 }
493 } else {
494 self.write_rgb(color);
495 }
496 } else if color.format != ColorFormat::Infer {
497 match &color.format {
498 ColorFormat::Rgb => self.write_rgb(color),
499 ColorFormat::Hsl => self.write_hsl(color),
500 ColorFormat::Literal(text) => self.buffer.extend_from_slice(text.as_bytes()),
501 ColorFormat::Infer => unreachable!(),
502 }
503 } else if name.is_some() && !fuzzy_equals(color.alpha().0, 0.0) {
506 self.buffer.extend_from_slice(name.unwrap().as_bytes());
507 } else if fuzzy_equals(color.alpha().0, 1.0) {
508 self.buffer.push(b'#');
509 self.write_hex_component(red as u32);
510 self.write_hex_component(green as u32);
511 self.write_hex_component(blue as u32);
512 } else {
513 self.write_rgb(color);
514 }
515 }
516
517 fn write_media_query(&mut self, query: &MediaQuery) {
518 if let Some(modifier) = &query.modifier {
519 self.buffer.extend_from_slice(modifier.as_bytes());
520 self.buffer.push(b' ');
521 }
522
523 if let Some(media_type) = &query.media_type {
524 self.buffer.extend_from_slice(media_type.as_bytes());
525
526 if !query.conditions.is_empty() {
527 self.buffer.extend_from_slice(b" and ");
528 }
529 }
530
531 if query.conditions.len() == 1 && query.conditions.first().unwrap().starts_with("(not ") {
532 self.buffer.extend_from_slice(b"not ");
533 let condition = query.conditions.first().unwrap();
534 self.buffer
535 .extend_from_slice(condition["(not ".len()..condition.len() - 1].as_bytes());
536 } else {
537 let operator = if query.conjunction { " and " } else { " or " };
538 self.buffer
539 .extend_from_slice(query.conditions.join(operator).as_bytes());
540 }
541 }
542
543 pub fn visit_number(&mut self, number: &SassNumber) -> SassResult<()> {
544 if let Some(as_slash) = &number.as_slash {
545 self.visit_number(&as_slash.0)?;
546 self.buffer.push(b'/');
547 self.visit_number(&as_slash.1)?;
548 return Ok(());
549 }
550
551 if !self.inspect && number.unit.is_complex() {
552 return Err((
553 format!(
554 "{} isn't a valid CSS value.",
555 inspect_number(number, self.options, self.span)?
556 ),
557 self.span,
558 )
559 .into());
560 }
561
562 self.write_float(number.num.0);
563 write!(&mut self.buffer, "{}", number.unit)?;
564
565 Ok(())
566 }
567
568 fn write_float(&mut self, float: f64) {
569 if float.is_infinite() && float.is_sign_negative() {
570 self.buffer.extend_from_slice(b"-Infinity");
571 return;
572 } else if float.is_infinite() {
573 self.buffer.extend_from_slice(b"Infinity");
574 return;
575 }
576
577 let mut buffer = String::with_capacity(3);
579
580 if float < 0.0 {
581 buffer.push('-');
582 }
583
584 let num = float.abs();
585
586 if self.options.is_compressed() && num < 1.0 {
587 buffer.push_str(
588 format!("{:.10}", num)[1..]
589 .trim_end_matches('0')
590 .trim_end_matches('.'),
591 );
592 } else {
593 buffer.push_str(
594 format!("{:.10}", num)
595 .trim_end_matches('0')
596 .trim_end_matches('.'),
597 );
598 }
599
600 if buffer.is_empty() || buffer == "-" || buffer == "-0" {
601 buffer = "0".to_owned();
602 }
603
604 self.buffer.append(&mut buffer.into_bytes());
605 }
606
607 pub fn visit_group(
608 &mut self,
609 stmt: CssStmt,
610 prev_was_group_end: bool,
611 prev_requires_semicolon: bool,
612 ) -> SassResult<()> {
613 if prev_requires_semicolon {
614 self.buffer.push(b';');
615 }
616
617 if !self.buffer.is_empty() {
618 self.write_optional_newline();
619 }
620
621 if prev_was_group_end && !self.buffer.is_empty() {
622 self.write_optional_newline();
623 }
624
625 self.visit_stmt(stmt)?;
626
627 Ok(())
628 }
629
630 fn finish_for_expr(self) -> String {
631 unsafe { String::from_utf8_unchecked(self.buffer) }
633 }
634
635 pub fn finish(mut self, prev_requires_semicolon: bool) -> String {
636 let is_not_ascii = self.buffer.iter().any(|&c| !c.is_ascii());
637
638 if prev_requires_semicolon {
639 self.buffer.push(b';');
640 }
641
642 if !self.buffer.is_empty() {
643 self.write_optional_newline();
644 }
645
646 let mut as_string = unsafe { String::from_utf8_unchecked(self.buffer) };
648
649 if is_not_ascii && self.options.is_compressed() && self.options.allows_charset {
650 as_string.insert(0, '\u{FEFF}');
651 } else if is_not_ascii && self.options.allows_charset {
652 as_string.insert_str(0, "@charset \"UTF-8\";\n");
653 }
654
655 as_string
656 }
657
658 fn write_indentation(&mut self) {
659 if self.options.is_compressed() {
660 return;
661 }
662
663 self.buffer.reserve(self.indentation);
664 for _ in 0..self.indentation {
665 self.buffer.push(b' ');
666 }
667 }
668
669 fn write_list_separator(&mut self, sep: ListSeparator) {
670 match (sep, self.options.is_compressed()) {
671 (ListSeparator::Space | ListSeparator::Undecided, _) => self.buffer.push(b' '),
672 (ListSeparator::Comma, true) => self.buffer.push(b','),
673 (ListSeparator::Comma, false) => self.buffer.extend_from_slice(b", "),
674 (ListSeparator::Slash, true) => self.buffer.push(b'/'),
675 (ListSeparator::Slash, false) => self.buffer.extend_from_slice(b" / "),
676 }
677 }
678
679 fn elem_needs_parens(sep: ListSeparator, elem: &Value) -> bool {
680 match elem {
681 Value::List(elems, sep2, brackets) => {
682 if elems.len() < 2 {
683 return false;
684 }
685
686 if *brackets == Brackets::Bracketed {
687 return false;
688 }
689
690 match sep {
691 ListSeparator::Comma => *sep2 == ListSeparator::Comma,
692 ListSeparator::Slash => {
693 *sep2 == ListSeparator::Comma || *sep2 == ListSeparator::Slash
694 }
695 _ => *sep2 != ListSeparator::Undecided,
696 }
697 }
698 _ => false,
699 }
700 }
701
702 fn visit_list(
703 &mut self,
704 list_elems: &[Value],
705 sep: ListSeparator,
706 brackets: Brackets,
707 span: Span,
708 ) -> SassResult<()> {
709 if brackets == Brackets::Bracketed {
710 self.buffer.push(b'[');
711 } else if list_elems.is_empty() {
712 if !self.inspect {
713 return Err(("() isn't a valid CSS value.", span).into());
714 }
715
716 self.buffer.extend_from_slice(b"()");
717 return Ok(());
718 }
719
720 let is_singleton = self.inspect
721 && list_elems.len() == 1
722 && (sep == ListSeparator::Comma || sep == ListSeparator::Slash);
723
724 if is_singleton && brackets != Brackets::Bracketed {
725 self.buffer.push(b'(');
726 }
727
728 let (mut x, mut y);
729 let elems: &mut dyn Iterator<Item = &Value> = if self.inspect {
730 x = list_elems.iter();
731 &mut x
732 } else {
733 y = list_elems.iter().filter(|elem| !elem.is_blank());
734 &mut y
735 };
736
737 let mut elems = elems.peekable();
738
739 while let Some(elem) = elems.next() {
740 if self.inspect {
741 let needs_parens = Self::elem_needs_parens(sep, elem);
742 if needs_parens {
743 self.buffer.push(b'(');
744 }
745
746 self.visit_value(elem, span)?;
747
748 if needs_parens {
749 self.buffer.push(b')');
750 }
751 } else {
752 self.visit_value(elem, span)?;
753 }
754
755 if elems.peek().is_some() {
756 self.write_list_separator(sep);
757 }
758 }
759
760 if is_singleton {
761 match sep {
762 ListSeparator::Comma => self.buffer.push(b','),
763 ListSeparator::Slash => self.buffer.push(b'/'),
764 _ => unreachable!(),
765 }
766
767 if brackets != Brackets::Bracketed {
768 self.buffer.push(b')');
769 }
770 }
771
772 if brackets == Brackets::Bracketed {
773 self.buffer.push(b']');
774 }
775
776 Ok(())
777 }
778
779 fn write_map_element(&mut self, value: &Value, span: Span) -> SassResult<()> {
780 let needs_parens = matches!(value, Value::List(_, ListSeparator::Comma, Brackets::None));
781
782 if needs_parens {
783 self.buffer.push(b'(');
784 }
785
786 self.visit_value(value, span)?;
787
788 if needs_parens {
789 self.buffer.push(b')');
790 }
791
792 Ok(())
793 }
794
795 fn visit_map(&mut self, map: &SassMap, span: Span) -> SassResult<()> {
796 if !self.inspect {
797 return Err((
798 format!(
799 "{} isn't a valid CSS value.",
800 inspect_map(map, self.options, span)?
801 ),
802 span,
803 )
804 .into());
805 }
806
807 self.buffer.push(b'(');
808
809 let mut elems = map.iter().peekable();
810
811 while let Some((k, v)) = elems.next() {
812 self.write_map_element(&k.node, k.span)?;
813 self.buffer.extend_from_slice(b": ");
814 self.write_map_element(v, k.span)?;
815 if elems.peek().is_some() {
816 self.buffer.extend_from_slice(b", ");
817 }
818 }
819
820 self.buffer.push(b')');
821
822 Ok(())
823 }
824
825 fn visit_unquoted_string(&mut self, string: &str) {
826 let mut after_newline = false;
827 self.buffer.reserve(string.len());
828
829 for c in string.bytes() {
830 match c {
831 b'\n' => {
832 self.buffer.push(b' ');
833 after_newline = true;
834 }
835 b' ' => {
836 if !after_newline {
837 self.buffer.push(b' ');
838 }
839 }
840 _ => {
841 self.buffer.push(c);
842 after_newline = false;
843 }
844 }
845 }
846 }
847
848 fn visit_quoted_string(&mut self, force_double_quote: bool, string: &str) {
849 let mut has_single_quote = false;
850 let mut has_double_quote = false;
851
852 let mut buffer = Vec::new();
853
854 if force_double_quote {
855 buffer.push(b'"');
856 }
857 let mut iter = string.as_bytes().iter().copied().peekable();
858 while let Some(c) = iter.next() {
859 match c {
860 b'\'' => {
861 if force_double_quote {
862 buffer.push(b'\'');
863 } else if has_double_quote {
864 self.visit_quoted_string(true, string);
865 return;
866 } else {
867 has_single_quote = true;
868 buffer.push(b'\'');
869 }
870 }
871 b'"' => {
872 if force_double_quote {
873 buffer.push(b'\\');
874 buffer.push(b'"');
875 } else if has_single_quote {
876 self.visit_quoted_string(true, string);
877 return;
878 } else {
879 has_double_quote = true;
880 buffer.push(b'"');
881 }
882 }
883 b'\x00'..=b'\x08' | b'\x0A'..=b'\x1F' => {
884 buffer.push(b'\\');
885 if c as u32 > 0xF {
886 buffer.push(hex_char_for(c as u32 >> 4) as u8);
887 }
888 buffer.push(hex_char_for(c as u32 & 0xF) as u8);
889
890 let next = match iter.peek() {
891 Some(v) => *v,
892 None => break,
893 };
894
895 if next.is_ascii_hexdigit() || next == b' ' || next == b'\t' {
896 buffer.push(b' ');
897 }
898 }
899 b'\\' => {
900 buffer.push(b'\\');
901 buffer.push(b'\\');
902 }
903 _ => buffer.push(c),
904 }
905 }
906
907 if force_double_quote {
908 buffer.push(b'"');
909 self.buffer.extend_from_slice(&buffer);
910 } else {
911 let quote = if has_double_quote { b'\'' } else { b'"' };
912 self.buffer.push(quote);
913 self.buffer.extend_from_slice(&buffer);
914 self.buffer.push(quote);
915 }
916 }
917
918 fn visit_function_ref(&mut self, func: &SassFunction, span: Span) -> SassResult<()> {
919 if !self.inspect {
920 return Err((
921 format!(
922 "{} isn't a valid CSS value.",
923 inspect_function_ref(func, self.options, span)?
924 ),
925 span,
926 )
927 .into());
928 }
929
930 self.buffer.extend_from_slice(b"get-function(");
931 self.visit_quoted_string(false, func.name().as_str());
932 self.buffer.push(b')');
933
934 Ok(())
935 }
936
937 fn visit_arglist(&mut self, arglist: &ArgList, span: Span) -> SassResult<()> {
938 self.visit_list(&arglist.elems, ListSeparator::Comma, Brackets::None, span)
939 }
940
941 fn visit_value(&mut self, value: &Value, span: Span) -> SassResult<()> {
942 match value {
943 Value::Dimension(num) => self.visit_number(num)?,
944 Value::Color(color) => self.visit_color(color),
945 Value::Calculation(calc) => self.visit_calculation(calc)?,
946 Value::List(elems, sep, brackets) => self.visit_list(elems, *sep, *brackets, span)?,
947 Value::True => self.buffer.extend_from_slice(b"true"),
948 Value::False => self.buffer.extend_from_slice(b"false"),
949 Value::Null => {
950 if self.inspect {
951 self.buffer.extend_from_slice(b"null")
952 }
953 }
954 Value::Map(map) => self.visit_map(map, span)?,
955 Value::FunctionRef(func) => self.visit_function_ref(func, span)?,
956 Value::String(s, QuoteKind::Quoted) => self.visit_quoted_string(false, s),
957 Value::String(s, QuoteKind::None) => self.visit_unquoted_string(s),
958 Value::ArgList(arglist) => self.visit_arglist(arglist, span)?,
959 }
960
961 Ok(())
962 }
963
964 fn write_style(&mut self, style: Style) -> SassResult<()> {
965 if !self.options.is_compressed() {
966 self.write_indentation();
967 }
968
969 self.buffer
970 .extend_from_slice(style.property.resolve_ref().as_bytes());
971 self.buffer.push(b':');
972
973 if !style.declared_as_custom_property && !self.options.is_compressed() {
975 self.buffer.push(b' ');
976 }
977
978 self.visit_value(&style.value.node, style.value.span)?;
979
980 Ok(())
981 }
982
983 fn write_import(&mut self, import: &str, modifiers: Option<String>) -> SassResult<()> {
984 self.write_indentation();
985 self.buffer.extend_from_slice(b"@import ");
986 write!(&mut self.buffer, "{}", import)?;
987
988 if let Some(modifiers) = modifiers {
989 self.buffer.push(b' ');
990 self.buffer.extend_from_slice(modifiers.as_bytes());
991 }
992
993 Ok(())
994 }
995
996 fn write_comment(&mut self, comment: &str, span: Span) -> SassResult<()> {
997 if self.options.is_compressed() && !comment.starts_with("/*!") {
998 return Ok(());
999 }
1000
1001 self.write_indentation();
1002 let col = self.map.look_up_pos(span.low()).position.column;
1003 let mut lines = comment.lines();
1004
1005 if let Some(line) = lines.next() {
1006 self.buffer.extend_from_slice(line.trim_start().as_bytes());
1007 }
1008
1009 let lines = lines
1010 .map(|line| {
1011 let diff = (line.len() - line.trim_start().len()).saturating_sub(col);
1012 format!("{}{}", " ".repeat(diff), line.trim_start())
1013 })
1014 .collect::<Vec<String>>()
1015 .join("\n");
1016
1017 if !lines.is_empty() {
1018 write!(&mut self.buffer, "\n{}", lines)?;
1019 }
1020
1021 Ok(())
1022 }
1023
1024 pub fn requires_semicolon(stmt: &CssStmt) -> bool {
1025 match stmt {
1026 CssStmt::Style(_) | CssStmt::Import(_, _) => true,
1027 CssStmt::UnknownAtRule(rule, _) => !rule.has_body,
1028 _ => false,
1029 }
1030 }
1031
1032 fn write_children(&mut self, mut children: Vec<CssStmt>) -> SassResult<()> {
1033 if self.options.is_compressed() {
1034 self.buffer.push(b'{');
1035 } else {
1036 self.buffer.extend_from_slice(b" {\n");
1037 }
1038
1039 self.indentation += self.indent_width;
1040
1041 let last = children.pop();
1042
1043 for child in children {
1044 let needs_semicolon = Self::requires_semicolon(&child);
1045 let did_write = self.visit_stmt(child)?;
1046
1047 if !did_write {
1048 continue;
1049 }
1050
1051 if needs_semicolon {
1052 self.buffer.push(b';');
1053 }
1054
1055 self.write_optional_newline();
1056 }
1057
1058 if let Some(last) = last {
1059 let needs_semicolon = Self::requires_semicolon(&last);
1060 let did_write = self.visit_stmt(last)?;
1061
1062 if did_write {
1063 if needs_semicolon && !self.options.is_compressed() {
1064 self.buffer.push(b';');
1065 }
1066
1067 self.write_optional_newline();
1068 }
1069 }
1070
1071 self.indentation -= self.indent_width;
1072
1073 if self.options.is_compressed() {
1074 self.buffer.push(b'}');
1075 } else {
1076 self.write_indentation();
1077 self.buffer.extend_from_slice(b"}");
1078 }
1079
1080 Ok(())
1081 }
1082
1083 fn write_optional_space(&mut self) {
1084 if !self.options.is_compressed() {
1085 self.buffer.push(b' ');
1086 }
1087 }
1088
1089 fn write_optional_newline(&mut self) {
1090 if !self.options.is_compressed() {
1091 self.buffer.push(b'\n');
1092 }
1093 }
1094
1095 fn write_supports_rule(&mut self, supports_rule: SupportsRule) -> SassResult<()> {
1096 self.write_indentation();
1097 self.buffer.extend_from_slice(b"@supports");
1098
1099 if !supports_rule.params.is_empty() {
1100 self.buffer.push(b' ');
1101 self.buffer
1102 .extend_from_slice(supports_rule.params.as_bytes());
1103 }
1104
1105 self.write_children(supports_rule.body)?;
1106
1107 Ok(())
1108 }
1109
1110 fn visit_stmt(&mut self, stmt: CssStmt) -> SassResult<bool> {
1112 if stmt.is_invisible() {
1113 return Ok(false);
1114 }
1115
1116 match stmt {
1117 CssStmt::RuleSet { selector, body, .. } => {
1118 self.write_indentation();
1119 self.write_selector_list(&selector.as_selector_list());
1120
1121 self.write_children(body)?;
1122 }
1123 CssStmt::Media(media_rule, ..) => {
1124 self.write_indentation();
1125 self.buffer.extend_from_slice(b"@media ");
1126
1127 if let Some((last, rest)) = media_rule.query.split_last() {
1128 for query in rest {
1129 self.write_media_query(query);
1130
1131 self.buffer.push(b',');
1132
1133 self.write_optional_space();
1134 }
1135
1136 self.write_media_query(last);
1137 }
1138
1139 self.write_children(media_rule.body)?;
1140 }
1141 CssStmt::UnknownAtRule(unknown_at_rule, ..) => {
1142 self.write_indentation();
1143 self.buffer.push(b'@');
1144 self.buffer
1145 .extend_from_slice(unknown_at_rule.name.as_bytes());
1146
1147 if !unknown_at_rule.params.is_empty() {
1148 write!(&mut self.buffer, " {}", unknown_at_rule.params)?;
1149 }
1150
1151 if !unknown_at_rule.has_body {
1152 debug_assert!(unknown_at_rule.body.is_empty());
1153 return Ok(true);
1154 } else if unknown_at_rule.body.iter().all(CssStmt::is_invisible) {
1155 self.buffer.extend_from_slice(b" {}");
1156 return Ok(true);
1157 }
1158
1159 self.write_children(unknown_at_rule.body)?;
1160 }
1161 CssStmt::Style(style) => self.write_style(style)?,
1162 CssStmt::Comment(comment, span) => self.write_comment(&comment, span)?,
1163 CssStmt::KeyframesRuleSet(keyframes_rule_set) => {
1164 self.write_indentation();
1165 let selector = keyframes_rule_set
1167 .selector
1168 .into_iter()
1169 .map(|s| s.to_string())
1170 .collect::<Vec<String>>()
1171 .join(", ");
1172
1173 self.buffer.extend_from_slice(selector.as_bytes());
1174
1175 self.write_children(keyframes_rule_set.body)?;
1176 }
1177 CssStmt::Import(import, modifier) => self.write_import(&import, modifier)?,
1178 CssStmt::Supports(supports_rule, _) => self.write_supports_rule(supports_rule)?,
1179 }
1180
1181 Ok(true)
1182 }
1183}