malva/doc_gen/
sass.rs

1use super::{
2    helpers,
3    str::{
4        format_str, InterpolatedFirstStrRawFormatter, InterpolatedLastStrRawFormatter,
5        InterpolatedMidStrRawFormatter,
6    },
7    DocGen,
8};
9use crate::{ctx::Ctx, state::State};
10use raffia::{ast::*, Spanned};
11use std::{iter, mem};
12use tiny_pretty::Doc;
13
14impl<'s> DocGen<'s> for SassArbitraryArgument<'s> {
15    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
16        self.value.doc(ctx, state).append(Doc::text("..."))
17    }
18}
19
20impl<'s> DocGen<'s> for SassArbitraryParameter<'s> {
21    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
22        self.name.doc(ctx, state).append(Doc::text("..."))
23    }
24}
25
26impl<'s> DocGen<'s> for SassAtRoot<'s> {
27    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
28        match &self.kind {
29            SassAtRootKind::Selector(selector) => selector.doc(ctx, state),
30            SassAtRootKind::Query(query) => query.doc(ctx, state),
31        }
32    }
33}
34
35impl<'s> DocGen<'s> for SassAtRootQuery<'s> {
36    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
37        let mut docs = Vec::with_capacity(7);
38        docs.push(Doc::text("("));
39        docs.extend(ctx.end_spaced_comments(
40            ctx.get_comments_between(self.span.start, self.modifier.span.start),
41        ));
42
43        docs.push(self.modifier.doc(ctx, state));
44        docs.extend(ctx.start_spaced_comments(
45            ctx.get_comments_between(self.modifier.span.end, self.colon_span.start),
46        ));
47        docs.push(Doc::text(": "));
48
49        docs.extend(
50            itertools::intersperse(
51                self.rules.iter().scan(self.colon_span.start, |pos, rule| {
52                    let rule_span = rule.span();
53                    Some(
54                        ctx.end_spaced_comments(ctx.get_comments_between(
55                            mem::replace(pos, rule_span.end),
56                            rule_span.start,
57                        ))
58                        .chain(iter::once(rule.doc(ctx, state)))
59                        .collect::<Vec<_>>()
60                        .into_iter(),
61                    )
62                }),
63                vec![Doc::soft_line()].into_iter(),
64            )
65            .flatten(),
66        );
67
68        if let Some(last) = self.rules.last() {
69            docs.extend(
70                ctx.start_spaced_comments(ctx.get_comments_between(last.span().end, self.span.end)),
71            );
72        }
73
74        docs.push(Doc::text(")"));
75        Doc::list(docs)
76    }
77}
78
79impl<'s> DocGen<'s> for SassAtRootQueryModifier {
80    fn doc(&self, _: &Ctx<'_, 's>, _: &State) -> Doc<'s> {
81        match self.kind {
82            SassAtRootQueryModifierKind::With => Doc::text("with"),
83            SassAtRootQueryModifierKind::Without => Doc::text("without"),
84        }
85    }
86}
87
88impl<'s> DocGen<'s> for SassAtRootQueryRule<'s> {
89    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
90        match self {
91            SassAtRootQueryRule::Ident(ident) => ident.doc(ctx, state),
92            SassAtRootQueryRule::Str(str) => str.doc(ctx, state),
93        }
94    }
95}
96
97impl<'s> DocGen<'s> for SassBinaryExpression<'s> {
98    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
99        self.left
100            .doc(ctx, state)
101            .append(helpers::format_operator_prefix_space(ctx))
102            .concat(ctx.end_spaced_comments(
103                ctx.get_comments_between(self.left.span().end, self.op.span.start),
104            ))
105            .append(self.op.doc(ctx, state))
106            .append(helpers::format_operator_suffix_space(ctx))
107            .concat(ctx.end_spaced_comments(
108                ctx.get_comments_between(self.op.span.end, self.right.span().start),
109            ))
110            .append(self.right.doc(ctx, state))
111            .group()
112    }
113}
114
115impl<'s> DocGen<'s> for SassBinaryOperator {
116    fn doc(&self, _: &Ctx<'_, 's>, _: &State) -> Doc<'s> {
117        Doc::text(match self.kind {
118            SassBinaryOperatorKind::Multiply => "*",
119            SassBinaryOperatorKind::Division => "/",
120            SassBinaryOperatorKind::Modulo => "%",
121            SassBinaryOperatorKind::Plus => "+",
122            SassBinaryOperatorKind::Minus => "-",
123            SassBinaryOperatorKind::GreaterThan => ">",
124            SassBinaryOperatorKind::GreaterThanOrEqual => ">=",
125            SassBinaryOperatorKind::LessThan => "<",
126            SassBinaryOperatorKind::LessThanOrEqual => "<=",
127            SassBinaryOperatorKind::EqualsEquals => "==",
128            SassBinaryOperatorKind::ExclamationEquals => "!=",
129            SassBinaryOperatorKind::And => "and",
130            SassBinaryOperatorKind::Or => "or",
131        })
132    }
133}
134
135impl<'s> DocGen<'s> for SassConditionalClause<'s> {
136    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
137        self.condition
138            .doc(ctx, state)
139            .append(helpers::format_space_before_block(
140                self.condition.span().end,
141                self.block.span.start,
142                ctx,
143            ))
144            .append(self.block.doc(ctx, state))
145    }
146}
147
148impl<'s> DocGen<'s> for SassContent<'s> {
149    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
150        helpers::format_parenthesized(
151            helpers::SeparatedListFormatter::new(
152                ",",
153                helpers::get_smart_linebreak(
154                    self.span.start,
155                    &self.args,
156                    ctx.options.sass_content_at_rule_prefer_single_line,
157                    ctx,
158                ),
159            )
160            .with_trailing()
161            .format(&self.args, &self.comma_spans, self.span.start, ctx, state),
162            self.args
163                .len()
164                .checked_sub(1)
165                .and_then(|i| self.comma_spans.get(i))
166                .or_else(|| self.args.last().map(|param| param.span()))
167                .map(|span| span.end)
168                .unwrap_or(self.span.start),
169            self.span.end,
170            ctx,
171        )
172    }
173}
174
175impl<'s> DocGen<'s> for SassEach<'s> {
176    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
177        helpers::SeparatedListFormatter::new(",", Doc::line_or_space())
178            .format(
179                &self.bindings,
180                &self.comma_spans,
181                self.span.start,
182                ctx,
183                state,
184            )
185            .group()
186            .nest(ctx.indent_width)
187            .append(helpers::format_operator_prefix_space(ctx))
188            .concat(ctx.end_spaced_comments(
189                ctx.get_comments_between(
190                    self.bindings.last().unwrap().span.end,
191                    self.in_span.start,
192                ),
193            ))
194            .append(Doc::text("in"))
195            .append(helpers::format_operator_suffix_space(ctx))
196            .concat(ctx.end_spaced_comments(
197                ctx.get_comments_between(self.in_span.end, self.expr.span().start),
198            ))
199            .append(self.expr.doc(ctx, state).nest(ctx.indent_width))
200            .group()
201    }
202}
203
204impl<'s> DocGen<'s> for SassExtend<'s> {
205    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
206        let selectors =
207            helpers::SeparatedListFormatter::new(",", Doc::line_or_space().nest(ctx.indent_width))
208                .format(
209                    &self.selectors.selectors,
210                    &self.selectors.comma_spans,
211                    self.selectors.span.start,
212                    ctx,
213                    state,
214                )
215                .group();
216        if let Some(optional) = &self.optional {
217            selectors
218                .append(Doc::space())
219                .concat(ctx.end_spaced_comments(
220                    ctx.get_comments_between(self.selectors.span().end, optional.span.start),
221                ))
222                .append(optional.doc(ctx, state))
223        } else {
224            selectors
225        }
226    }
227}
228
229impl<'s> DocGen<'s> for SassFlag<'s> {
230    fn doc(&self, _: &Ctx<'_, 's>, _: &State) -> Doc<'s> {
231        Doc::text(format!("!{}", self.keyword.raw))
232    }
233}
234
235impl<'s> DocGen<'s> for SassFor<'s> {
236    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
237        use crate::config::OperatorLineBreak;
238
239        let start_value_span = self.start.span();
240        self.binding
241            .doc(ctx, state)
242            .append(match ctx.options.operator_linebreak {
243                OperatorLineBreak::Before => Doc::soft_line().nest(ctx.indent_width),
244                OperatorLineBreak::After => Doc::space(),
245            })
246            .concat(ctx.end_spaced_comments(
247                ctx.get_comments_between(self.binding.span.end, self.from_span.start),
248            ))
249            .append(Doc::text("from"))
250            .append(match ctx.options.operator_linebreak {
251                OperatorLineBreak::Before => Doc::space(),
252                OperatorLineBreak::After => Doc::soft_line().nest(ctx.indent_width),
253            })
254            .concat(ctx.end_spaced_comments(
255                ctx.get_comments_between(self.from_span.end, start_value_span.start),
256            ))
257            .append(self.start.doc(ctx, state))
258            .append(match ctx.options.operator_linebreak {
259                OperatorLineBreak::Before => Doc::soft_line().nest(ctx.indent_width),
260                OperatorLineBreak::After => Doc::space(),
261            })
262            .concat(ctx.end_spaced_comments(
263                ctx.get_comments_between(start_value_span.end, self.boundary.span.start),
264            ))
265            .append(self.boundary.doc(ctx, state))
266            .append(match ctx.options.operator_linebreak {
267                OperatorLineBreak::Before => Doc::space(),
268                OperatorLineBreak::After => Doc::soft_line().nest(ctx.indent_width),
269            })
270            .concat(ctx.end_spaced_comments(
271                ctx.get_comments_between(self.boundary.span.end, self.end.span().start),
272            ))
273            .append(self.end.doc(ctx, state))
274    }
275}
276
277impl<'s> DocGen<'s> for SassForBoundary {
278    fn doc(&self, _: &Ctx<'_, 's>, _: &State) -> Doc<'s> {
279        match self.kind {
280            SassForBoundaryKind::Exclusive => Doc::text("to"),
281            SassForBoundaryKind::Inclusive => Doc::text("through"),
282        }
283    }
284}
285
286impl<'s> DocGen<'s> for SassForward<'s> {
287    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
288        let mut docs = vec![self.path.doc(ctx, state)];
289        let mut pos = self.path.span().end;
290
291        if let Some(prefix) = &self.prefix {
292            docs.reserve(2);
293            docs.push(Doc::space());
294            docs.extend(ctx.end_spaced_comments(
295                ctx.get_comments_between(
296                    mem::replace(&mut pos, prefix.span.end),
297                    prefix.span.start,
298                ),
299            ));
300            docs.push(prefix.doc(ctx, state));
301        }
302
303        if let Some(visibility) = &self.visibility {
304            docs.reserve(2);
305            docs.push(Doc::space());
306            docs.extend(ctx.end_spaced_comments(ctx.get_comments_between(
307                mem::replace(&mut pos, visibility.span.end),
308                visibility.span.start,
309            )));
310            docs.push(visibility.doc(ctx, state));
311        }
312
313        if let Some(config) = &self.config {
314            docs.reserve(2);
315            docs.push(Doc::space());
316            docs.extend(ctx.end_spaced_comments(
317                ctx.get_comments_between(
318                    mem::replace(&mut pos, config.span.end),
319                    config.span.start,
320                ),
321            ));
322            docs.push(config.doc(ctx, state));
323        }
324
325        Doc::list(docs)
326    }
327}
328
329impl<'s> DocGen<'s> for SassForwardMember<'s> {
330    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
331        match self {
332            SassForwardMember::Ident(ident) => ident.doc(ctx, state),
333            SassForwardMember::Variable(variable) => variable.doc(ctx, state),
334        }
335    }
336}
337
338impl<'s> DocGen<'s> for SassForwardPrefix<'s> {
339    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
340        Doc::text("as ")
341            .concat(ctx.end_spaced_comments(
342                ctx.get_comments_between(self.as_span.end, self.name.span.start),
343            ))
344            .append(self.name.doc(ctx, state))
345            .append(Doc::text("*"))
346    }
347}
348
349impl<'s> DocGen<'s> for SassForwardVisibility<'s> {
350    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
351        self.modifier.doc(ctx, state).append(Doc::space()).append(
352            helpers::SeparatedListFormatter::new(",", Doc::soft_line()).format(
353                &self.members,
354                &self.comma_spans,
355                self.modifier.span.end,
356                ctx,
357                state,
358            ),
359        )
360    }
361}
362
363impl<'s> DocGen<'s> for SassForwardVisibilityModifier {
364    fn doc(&self, _: &Ctx<'_, 's>, _: &State) -> Doc<'s> {
365        match self.kind {
366            SassForwardVisibilityModifierKind::Hide => Doc::text("hide"),
367            SassForwardVisibilityModifierKind::Show => Doc::text("show"),
368        }
369    }
370}
371
372impl<'s> DocGen<'s> for SassFunction<'s> {
373    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
374        self.name
375            .doc(ctx, state)
376            .append(self.parameters.doc(ctx, state))
377    }
378}
379
380impl<'s> DocGen<'s> for SassIfAtRule<'s> {
381    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
382        let mut docs = vec![Doc::text("@if ")];
383        docs.extend(ctx.end_spaced_comments(
384            ctx.get_comments_between(self.span.start, self.if_clause.span.start),
385        ));
386        docs.push(self.if_clause.doc(ctx, state));
387        let mut pos = self.if_clause.span.end;
388
389        docs.extend(
390            self.else_if_clauses
391                .iter()
392                .zip(self.else_spans.iter())
393                .scan(&mut pos, |pos, (clause, elseif_span)| {
394                    Some(
395                        iter::once(Doc::space())
396                            .chain(ctx.end_spaced_comments(ctx.get_comments_between(
397                                mem::replace(*pos, elseif_span.end),
398                                elseif_span.start,
399                            )))
400                            .chain(iter::once(Doc::text("@else if ")))
401                            .chain(ctx.end_spaced_comments(ctx.get_comments_between(
402                                mem::replace(*pos, clause.span.end),
403                                clause.span.start,
404                            )))
405                            .chain(iter::once(clause.doc(ctx, state))),
406                    )
407                })
408                .flatten(),
409        );
410
411        if let Some((else_clause, else_span)) =
412            self.else_clause.as_ref().zip(self.else_spans.last())
413        {
414            docs.reserve(3);
415            docs.push(Doc::space());
416            docs.extend(ctx.end_spaced_comments(ctx.get_comments_between(pos, else_span.start)));
417            docs.push(Doc::text("@else"));
418            docs.push(helpers::format_space_before_block(
419                else_span.end,
420                else_clause.span.start,
421                ctx,
422            ));
423            docs.push(else_clause.doc(ctx, state));
424        }
425
426        Doc::list(docs)
427    }
428}
429
430impl<'s> DocGen<'s> for SassImportPrelude<'s> {
431    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
432        helpers::SeparatedListFormatter::new(",", Doc::line_or_space().nest(ctx.indent_width))
433            .format(&self.paths, &self.comma_spans, self.span.start, ctx, state)
434            .group()
435    }
436}
437
438impl<'s> DocGen<'s> for SassInclude<'s> {
439    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
440        let mut docs = vec![self.name.doc(ctx, state)];
441        let mut pos = self.name.span().end;
442
443        if let Some(arguments) = &self.arguments {
444            docs.extend(ctx.end_spaced_comments(ctx.get_comments_between(
445                mem::replace(&mut pos, arguments.span.end),
446                arguments.span.start,
447            )));
448            docs.push(arguments.doc(ctx, state));
449        }
450
451        if let Some(content_block_params) = &self.content_block_params {
452            docs.reserve(2);
453            docs.push(Doc::space());
454            docs.extend(ctx.end_spaced_comments(ctx.get_comments_between(
455                mem::replace(&mut pos, content_block_params.span.end),
456                content_block_params.span.start,
457            )));
458            docs.push(content_block_params.doc(ctx, state));
459        }
460
461        Doc::list(docs)
462    }
463}
464
465impl<'s> DocGen<'s> for SassIncludeArgs<'s> {
466    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
467        helpers::format_parenthesized(
468            helpers::SeparatedListFormatter::new(
469                ",",
470                helpers::get_smart_linebreak(
471                    self.span.start,
472                    &self.args,
473                    ctx.options.sass_include_at_rule_prefer_single_line,
474                    ctx,
475                ),
476            )
477            .with_trailing()
478            .format(&self.args, &self.comma_spans, self.span.start, ctx, state),
479            self.args
480                .len()
481                .checked_sub(1)
482                .and_then(|i| self.comma_spans.get(i))
483                .or_else(|| self.args.last().map(|param| param.span()))
484                .map(|span| span.end)
485                .unwrap_or(self.span.start),
486            self.span.end,
487            ctx,
488        )
489    }
490}
491
492impl<'s> DocGen<'s> for SassIncludeContentBlockParams<'s> {
493    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
494        Doc::text("using ")
495            .concat(ctx.end_spaced_comments(
496                ctx.get_comments_between(self.using_span.end, self.params.span.start),
497            ))
498            .append(self.params.doc(ctx, state))
499    }
500}
501
502impl<'s> DocGen<'s> for SassInterpolatedIdent<'s> {
503    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
504        let mut docs = Vec::with_capacity(self.elements.len());
505        let mut iter = self.elements.iter().peekable();
506        let mut pos = self.span.start;
507        while let Some(element) = iter.next() {
508            match element {
509                SassInterpolatedIdentElement::Static(s) => {
510                    pos = s.span.end;
511                    docs.push(s.doc(ctx, state));
512                }
513                SassInterpolatedIdentElement::Expression(expr) => {
514                    let expr_span = expr.span();
515                    docs.push(Doc::text("#{"));
516                    docs.extend(
517                        ctx.end_spaced_comments(ctx.get_comments_between(pos, expr_span.start)),
518                    );
519                    docs.push(expr.doc(ctx, state));
520                    docs.extend(
521                        ctx.start_spaced_comments(
522                            ctx.get_comments_between(
523                                expr_span.end,
524                                iter.peek()
525                                    .map(|element| element.span().start)
526                                    .unwrap_or(self.span.end),
527                            ),
528                        ),
529                    );
530                    docs.push(Doc::text("}"));
531                }
532            }
533        }
534
535        Doc::list(docs)
536    }
537}
538
539impl<'s> DocGen<'s> for SassInterpolatedStr<'s> {
540    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
541        if let [SassInterpolatedStrElement::Static(first), mid @ .., SassInterpolatedStrElement::Static(last)] =
542            &self.elements[..]
543        {
544            let allow_prefer = is_preferred_quote_allowed(self, ctx);
545
546            let mut docs = Vec::with_capacity(self.elements.len());
547            docs.push(Doc::text(format_str(
548                first.raw,
549                InterpolatedFirstStrRawFormatter::new(first.raw),
550                allow_prefer,
551                ctx,
552            )));
553            let mut iter = mid.iter().peekable();
554            let mut pos = first.span.end;
555            while let Some(element) = iter.next() {
556                match element {
557                    SassInterpolatedStrElement::Static(s) => {
558                        pos = s.span.end;
559                        docs.push(Doc::text(format_str(
560                            s.raw,
561                            InterpolatedMidStrRawFormatter::new(s.raw),
562                            allow_prefer,
563                            ctx,
564                        )));
565                    }
566                    SassInterpolatedStrElement::Expression(expr) => {
567                        let expr_span = expr.span();
568                        docs.push(Doc::text("#{"));
569                        docs.extend(
570                            ctx.end_spaced_comments(ctx.get_comments_between(pos, expr_span.start)),
571                        );
572                        docs.push(expr.doc(ctx, state));
573                        docs.extend(
574                            ctx.start_spaced_comments(
575                                ctx.get_comments_between(
576                                    expr_span.end,
577                                    iter.peek()
578                                        .map(|element| element.span().start)
579                                        .unwrap_or(self.span.end),
580                                ),
581                            ),
582                        );
583                        docs.push(Doc::text("}"));
584                    }
585                }
586            }
587            docs.push(Doc::text(format_str(
588                last.raw,
589                InterpolatedLastStrRawFormatter::new(last.raw),
590                allow_prefer,
591                ctx,
592            )));
593            Doc::list(docs)
594        } else {
595            unreachable!()
596        }
597    }
598}
599
600impl<'s> DocGen<'s> for SassInterpolatedUrl<'s> {
601    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
602        let mut docs = Vec::with_capacity(self.elements.len());
603        let mut iter = self.elements.iter().peekable();
604        let mut pos = self.span.start;
605        while let Some(element) = iter.next() {
606            match element {
607                SassInterpolatedUrlElement::Static(s) => {
608                    pos = s.span.end;
609                    docs.push(s.doc(ctx, state));
610                }
611                SassInterpolatedUrlElement::Expression(expr) => {
612                    let expr_span = expr.span();
613                    docs.push(Doc::text("#{"));
614                    docs.extend(
615                        ctx.end_spaced_comments(ctx.get_comments_between(pos, expr_span.start)),
616                    );
617                    docs.push(expr.doc(ctx, state));
618                    docs.extend(
619                        ctx.start_spaced_comments(
620                            ctx.get_comments_between(
621                                expr_span.end,
622                                iter.peek()
623                                    .map(|element| element.span().start)
624                                    .unwrap_or(self.span.end),
625                            ),
626                        ),
627                    );
628                    docs.push(Doc::text("}"));
629                }
630            }
631        }
632
633        Doc::list(docs)
634    }
635}
636
637impl<'s> DocGen<'s> for SassKeywordArgument<'s> {
638    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
639        self.name
640            .doc(ctx, state)
641            .concat(ctx.start_spaced_comments(
642                ctx.get_comments_between(self.name.span.start, self.colon_span.start),
643            ))
644            .append(Doc::text(": "))
645            .concat(ctx.end_spaced_comments(
646                ctx.get_comments_between(self.colon_span.end, self.value.span().start),
647            ))
648            .append(self.value.doc(ctx, state))
649    }
650}
651
652impl<'s> DocGen<'s> for SassList<'s> {
653    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
654        helpers::format_values_list(
655            &self.elements,
656            self.comma_spans.as_deref(),
657            &self.span,
658            ctx,
659            state,
660        )
661    }
662}
663
664impl<'s> DocGen<'s> for SassMap<'s> {
665    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
666        helpers::format_parenthesized(
667            helpers::SeparatedListFormatter::new(
668                ",",
669                helpers::get_smart_linebreak(
670                    self.span.start,
671                    &self.items,
672                    ctx.options.sass_map_prefer_single_line,
673                    ctx,
674                ),
675            )
676            .with_trailing()
677            .format(&self.items, &self.comma_spans, self.span.start, ctx, state),
678            self.items
679                .last()
680                .map(|item| item.span.end)
681                .unwrap_or(self.span.start),
682            self.span.end,
683            ctx,
684        )
685    }
686}
687
688impl<'s> DocGen<'s> for SassMapItem<'s> {
689    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
690        self.key
691            .doc(ctx, state)
692            .concat(ctx.start_spaced_comments(
693                ctx.get_comments_between(self.key.span().end, self.colon_span.start),
694            ))
695            .append(Doc::text(": "))
696            .concat(ctx.end_spaced_comments(
697                ctx.get_comments_between(self.colon_span.end, self.value.span().start),
698            ))
699            .append(self.value.doc(ctx, state))
700    }
701}
702
703impl<'s> DocGen<'s> for SassMixin<'s> {
704    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
705        let name = self.name.doc(ctx, state);
706        if let Some(parameters) = &self.parameters {
707            name.append(parameters.doc(ctx, state))
708        } else {
709            name
710        }
711    }
712}
713
714impl<'s> DocGen<'s> for SassModuleConfig<'s> {
715    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
716        Doc::text("with ")
717            .concat(ctx.end_spaced_comments(
718                ctx.get_comments_between(self.with_span.end, self.lparen_span.start),
719            ))
720            .append(helpers::format_parenthesized(
721                helpers::SeparatedListFormatter::new(
722                    ",",
723                    helpers::get_smart_linebreak(
724                        self.span.start,
725                        &self.items,
726                        ctx.options.sass_module_config_prefer_single_line,
727                        ctx,
728                    ),
729                )
730                .with_trailing()
731                .format(
732                    &self.items,
733                    &self.comma_spans,
734                    self.lparen_span.end,
735                    ctx,
736                    state,
737                ),
738                self.items
739                    .last()
740                    .map(|item| item.span.end)
741                    .unwrap_or(self.lparen_span.end),
742                self.span.end,
743                ctx,
744            ))
745    }
746}
747
748impl<'s> DocGen<'s> for SassModuleConfigItem<'s> {
749    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
750        let value_span = self.value.span();
751        self.variable
752            .doc(ctx, state)
753            .concat(ctx.start_spaced_comments(
754                ctx.get_comments_between(self.variable.span.end, self.colon_span.start),
755            ))
756            .append(Doc::text(": "))
757            .concat(ctx.end_spaced_comments(
758                ctx.get_comments_between(self.colon_span.end, value_span.start),
759            ))
760            .append(self.value.doc(ctx, state))
761            .concat(
762                self.flags
763                    .iter()
764                    .scan(value_span.end, |pos, flag| {
765                        Some(
766                            iter::once(Doc::soft_line())
767                                .chain(ctx.end_spaced_comments(ctx.get_comments_between(
768                                    mem::replace(pos, flag.span.end),
769                                    flag.span.start,
770                                )))
771                                .chain(iter::once(flag.doc(ctx, state)))
772                                .collect::<Vec<_>>()
773                                .into_iter(),
774                        )
775                    })
776                    .flatten(),
777            )
778    }
779}
780
781impl<'s> DocGen<'s> for SassModuleMemberName<'s> {
782    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
783        match self {
784            SassModuleMemberName::Ident(ident) => ident.doc(ctx, state),
785            SassModuleMemberName::Variable(variable) => variable.doc(ctx, state),
786        }
787    }
788}
789
790impl<'s> DocGen<'s> for SassNestingDeclaration<'s> {
791    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
792        self.block.doc(ctx, state)
793    }
794}
795
796impl<'s> DocGen<'s> for SassParameter<'s> {
797    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
798        let name = self.name.doc(ctx, state);
799        if let Some(default_value) = &self.default_value {
800            name.concat(ctx.start_spaced_comments(
801                ctx.get_comments_between(self.name.span.end, default_value.span.start),
802            ))
803            .append(default_value.doc(ctx, state))
804        } else {
805            name
806        }
807    }
808}
809
810impl<'s> DocGen<'s> for SassParameterDefaultValue<'s> {
811    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
812        Doc::text(": ")
813            .concat(ctx.end_spaced_comments(
814                ctx.get_comments_between(self.colon_span.end, self.value.span().start),
815            ))
816            .append(self.value.doc(ctx, state))
817    }
818}
819
820impl<'s> DocGen<'s> for SassParameters<'s> {
821    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
822        enum ParameterOrArbitrary<'a, 's> {
823            Parameter(&'a SassParameter<'s>),
824            Arbitrary(&'a SassArbitraryParameter<'s>),
825        }
826        impl Spanned for ParameterOrArbitrary<'_, '_> {
827            fn span(&self) -> &raffia::Span {
828                match self {
829                    ParameterOrArbitrary::Parameter(p) => p.span(),
830                    ParameterOrArbitrary::Arbitrary(a) => a.span(),
831                }
832            }
833        }
834        impl<'s> DocGen<'s> for ParameterOrArbitrary<'_, 's> {
835            fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
836                match self {
837                    ParameterOrArbitrary::Parameter(p) => p.doc(ctx, state),
838                    ParameterOrArbitrary::Arbitrary(a) => a.doc(ctx, state),
839                }
840            }
841        }
842
843        let params = self
844            .params
845            .iter()
846            .map(ParameterOrArbitrary::Parameter)
847            .chain(
848                self.arbitrary_param
849                    .iter()
850                    .map(ParameterOrArbitrary::Arbitrary),
851            )
852            .collect::<Vec<_>>();
853        helpers::format_parenthesized(
854            helpers::SeparatedListFormatter::new(
855                ",",
856                helpers::get_smart_linebreak(
857                    self.span.start,
858                    &params,
859                    ctx.options.sass_params_prefer_single_line,
860                    ctx,
861                ),
862            )
863            .with_trailing()
864            .format(&params, &self.comma_spans, self.span.start, ctx, state),
865            self.params
866                .len()
867                .checked_sub(1)
868                .and_then(|i| self.comma_spans.get(i))
869                .or_else(|| self.params.last().map(|param| param.span()))
870                .map(|span| span.end)
871                .unwrap_or(self.span.start),
872            self.span.end,
873            ctx,
874        )
875    }
876}
877
878impl<'s> DocGen<'s> for SassParenthesizedExpression<'s> {
879    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
880        let expr_span = self.expr.span();
881        helpers::format_parenthesized(
882            Doc::list(
883                ctx.end_spaced_comments(ctx.get_comments_between(self.span.start, expr_span.start))
884                    .collect(),
885            )
886            .append(self.expr.doc(ctx, state)),
887            expr_span.end,
888            self.span.end,
889            ctx,
890        )
891    }
892}
893
894impl<'s> DocGen<'s> for SassPlaceholderSelector<'s> {
895    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
896        Doc::text("%").append(self.name.doc(ctx, state))
897    }
898}
899
900impl<'s> DocGen<'s> for SassQualifiedName<'s> {
901    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
902        Doc::list(vec![
903            self.module.doc(ctx, state),
904            Doc::text("."),
905            self.member.doc(ctx, state),
906        ])
907    }
908}
909
910impl<'s> DocGen<'s> for SassUnaryExpression<'s> {
911    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
912        self.op.doc(ctx, state).append(self.expr.doc(ctx, state))
913    }
914}
915
916impl<'s> DocGen<'s> for SassUnaryOperator {
917    fn doc(&self, _: &Ctx<'_, 's>, _: &State) -> Doc<'s> {
918        match self.kind {
919            SassUnaryOperatorKind::Plus => Doc::text("+"),
920            SassUnaryOperatorKind::Minus => Doc::text("-"),
921            SassUnaryOperatorKind::Not => Doc::text("not "),
922        }
923    }
924}
925
926impl<'s> DocGen<'s> for SassUnnamedNamespace {
927    fn doc(&self, _: &Ctx<'_, 's>, _: &State) -> Doc<'s> {
928        Doc::text("*")
929    }
930}
931
932impl<'s> DocGen<'s> for SassUse<'s> {
933    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
934        let mut docs = vec![self.path.doc(ctx, state)];
935        let mut pos = self.path.span().end;
936
937        if let Some(namespace) = &self.namespace {
938            docs.push(Doc::space());
939            docs.extend(ctx.end_spaced_comments(ctx.get_comments_between(
940                mem::replace(&mut pos, namespace.span.end),
941                namespace.span.start,
942            )));
943            docs.push(namespace.doc(ctx, state));
944        }
945
946        if let Some(config) = &self.config {
947            docs.push(Doc::space());
948            docs.extend(ctx.end_spaced_comments(ctx.get_comments_between(pos, config.span.start)));
949            docs.push(config.doc(ctx, state));
950        }
951
952        Doc::list(docs)
953    }
954}
955
956impl<'s> DocGen<'s> for SassUseNamespace<'s> {
957    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
958        Doc::text("as ")
959            .concat(ctx.end_spaced_comments(
960                ctx.get_comments_between(self.as_span.end, self.kind.span().start),
961            ))
962            .append(match &self.kind {
963                SassUseNamespaceKind::Named(named) => named.doc(ctx, state),
964                SassUseNamespaceKind::Unnamed(unnamed) => unnamed.doc(ctx, state),
965            })
966    }
967}
968
969impl<'s> DocGen<'s> for SassVariable<'s> {
970    fn doc(&self, _: &Ctx<'_, 's>, _: &State) -> Doc<'s> {
971        Doc::text(format!("${}", self.name.raw))
972    }
973}
974
975impl<'s> DocGen<'s> for SassVariableDeclaration<'s> {
976    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
977        let mut docs = Vec::with_capacity(3);
978        let value_span = self.value.span();
979
980        if let Some(namespace) = &self.namespace {
981            docs.push(namespace.doc(ctx, state));
982            docs.push(Doc::text("."));
983        }
984        docs.push(self.name.doc(ctx, state));
985
986        docs.extend(ctx.start_spaced_comments(
987            ctx.get_comments_between(self.name.span.end, self.colon_span.start),
988        ));
989        docs.push(Doc::text(":"));
990
991        let should_group = match &self.value {
992            ComponentValue::SassList(SassList {
993                elements,
994                comma_spans: Some(comma_spans),
995                span,
996                ..
997            }) => {
998                docs.push(Doc::line_or_space());
999                docs.extend(ctx.end_spaced_comments(
1000                    ctx.get_comments_between(self.colon_span.end, value_span.start),
1001                ));
1002                docs.push(
1003                    helpers::SeparatedListFormatter::new(",", Doc::line_or_space())
1004                        .with_trailing()
1005                        .format(elements, comma_spans, span.start, ctx, state),
1006                );
1007                if elements.len() == 1 {
1008                    docs.push(Doc::text(","));
1009                }
1010                true
1011            }
1012            ComponentValue::SassList(sass_list) => {
1013                docs.push(Doc::space());
1014                docs.extend(ctx.end_spaced_comments(
1015                    ctx.get_comments_between(self.colon_span.end, value_span.start),
1016                ));
1017                docs.push(sass_list.doc(ctx, state).nest(ctx.indent_width));
1018                false
1019            }
1020            _ => {
1021                docs.push(Doc::space());
1022                docs.extend(ctx.end_spaced_comments(
1023                    ctx.get_comments_between(self.colon_span.end, value_span.start),
1024                ));
1025                docs.push(self.value.doc(ctx, state));
1026                false
1027            }
1028        };
1029
1030        docs.extend(
1031            self.flags
1032                .iter()
1033                .scan(value_span.end, |pos, flag| {
1034                    Some(
1035                        iter::once(Doc::soft_line().nest(ctx.indent_width))
1036                            .chain(ctx.end_spaced_comments(ctx.get_comments_between(
1037                                mem::replace(pos, flag.span.end),
1038                                flag.span.start,
1039                            )))
1040                            .chain(iter::once(flag.doc(ctx, state)))
1041                            .collect::<Vec<_>>()
1042                            .into_iter(),
1043                    )
1044                })
1045                .flatten(),
1046        );
1047
1048        let doc = Doc::list(docs);
1049        if should_group {
1050            doc.group().nest(ctx.indent_width)
1051        } else {
1052            doc
1053        }
1054    }
1055}
1056
1057impl<'s> DocGen<'s> for UnknownSassAtRule<'s> {
1058    fn doc(&self, ctx: &Ctx<'_, 's>, state: &State) -> Doc<'s> {
1059        let mut docs = Vec::with_capacity(6);
1060        let mut pos = self.name.span().end;
1061
1062        docs.push(Doc::text("@"));
1063        docs.push(self.name.doc(ctx, state));
1064
1065        if let Some(prelude) = &self.prelude {
1066            docs.push(Doc::space());
1067            let span = prelude.span();
1068            docs.extend(ctx.end_spaced_comments(ctx.get_comments_between(pos, span.start)));
1069            docs.push(prelude.doc(ctx, state));
1070            pos = span.end;
1071        }
1072
1073        if let Some(block) = &self.block {
1074            docs.push(helpers::format_space_before_block(
1075                pos,
1076                block.span.start,
1077                ctx,
1078            ));
1079            docs.push(block.doc(ctx, state));
1080        }
1081
1082        Doc::list(docs)
1083    }
1084}
1085
1086fn is_preferred_quote_allowed(interpolated_str: &SassInterpolatedStr, ctx: &Ctx) -> bool {
1087    use crate::config::Quotes;
1088
1089    match ctx.options.quotes {
1090        Quotes::AlwaysDouble | Quotes::AlwaysSingle => false,
1091        Quotes::PreferDouble => interpolated_str
1092            .elements
1093            .iter()
1094            .any(|element| match element {
1095                SassInterpolatedStrElement::Static(InterpolableStrStaticPart { value, .. }) => {
1096                    value.contains('"')
1097                }
1098                SassInterpolatedStrElement::Expression(_) => false,
1099            }),
1100        Quotes::PreferSingle => interpolated_str
1101            .elements
1102            .iter()
1103            .any(|element| match element {
1104                SassInterpolatedStrElement::Static(InterpolableStrStaticPart { value, .. }) => {
1105                    value.contains('\'')
1106                }
1107                SassInterpolatedStrElement::Expression(_) => false,
1108            }),
1109    }
1110}