grass_compiler/
serializer.rs

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    // todo: use this field
129    _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 we emit an empty compound, it's because all of the components got
243        // optimized out because they match all selectors, so we just emit the
244        // universal selector.
245        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                // todo: avoid allocation with `write_binary_operator` method
369                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            // Always emit generated transparent colors in rgba format. This works
504            // around an IE bug. See sass/sass#1782.
505        } 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        // todo: can optimize away intermediate buffer
578        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        // SAFETY: todo
632        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        // SAFETY: todo
647        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        // todo: _writeFoldedValue and _writeReindentedValue
974        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    /// Returns whether or not text was written
1111    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                // todo: i bet we can do something like write_with_separator to avoid extra allocation
1166                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}