malva/doc_gen/
helpers.rs

1use super::DocGen;
2use crate::{ctx::Ctx, state::State};
3use itertools::{EitherOrBoth, Itertools};
4use raffia::{ast::*, Span, Spanned, Syntax};
5use std::{iter, mem};
6use tiny_pretty::Doc;
7
8pub(super) fn format_selectors_before_block<'s, N>(
9    selectors: &[N],
10    comma_spans: &[Span],
11    start: usize,
12    ctx: &Ctx<'_, 's>,
13    state: &State,
14) -> Doc<'s>
15where
16    N: DocGen<'s> + Spanned,
17{
18    use crate::{config::BlockSelectorLineBreak, state::SelectorOverride};
19
20    let linebreak = match state.selector_override {
21        SelectorOverride::Unset => ctx.options.block_selector_linebreak.clone(),
22        SelectorOverride::Ignore => {
23            if let Some(source) = ctx.source {
24                let first = selectors[0].span();
25                let raw = &source[first.start
26                    ..selectors
27                        .last()
28                        .map(|last| last.span())
29                        .unwrap_or(first)
30                        .end];
31                return Doc::list(
32                    itertools::intersperse(raw.lines().map(Doc::text), Doc::empty_line()).collect(),
33                );
34            } else {
35                ctx.options.block_selector_linebreak.clone()
36            }
37        }
38        SelectorOverride::Always => BlockSelectorLineBreak::Always,
39        SelectorOverride::Consistent => BlockSelectorLineBreak::Consistent,
40        SelectorOverride::Wrap => BlockSelectorLineBreak::Wrap,
41    };
42
43    SeparatedListFormatter::new(
44        ",",
45        match linebreak {
46            BlockSelectorLineBreak::Always => Doc::hard_line(),
47            BlockSelectorLineBreak::Consistent => {
48                if ctx
49                    .options
50                    .selectors_prefer_single_line
51                    .unwrap_or(ctx.options.prefer_single_line)
52                    || selectors
53                        .first()
54                        .zip(selectors.get(1))
55                        .is_some_and(|(first, second)| {
56                            ctx.line_bounds
57                                .line_distance(first.span().end, second.span().start)
58                                == 0
59                        })
60                {
61                    Doc::line_or_space()
62                } else {
63                    Doc::hard_line()
64                }
65            }
66            BlockSelectorLineBreak::Wrap => Doc::soft_line(),
67        },
68    )
69    .format(selectors, comma_spans, start, ctx, state)
70    .group()
71}
72
73pub(super) struct SeparatedListFormatter {
74    separator: Doc<'static>,
75    space_after_separator: Doc<'static>,
76    trailing: bool,
77}
78
79impl SeparatedListFormatter {
80    pub(super) fn new(separator: &'static str, space_after_separator: Doc<'static>) -> Self {
81        Self {
82            separator: Doc::text(separator),
83            space_after_separator,
84            trailing: false,
85        }
86    }
87
88    /// Remember to call `.group()` if enabling trailing separator,
89    /// otherwise it can't decide whether to add or not.
90    pub(super) fn with_trailing(mut self) -> Self {
91        self.trailing = true;
92        self
93    }
94
95    pub(super) fn format<'s, N>(
96        self,
97        list: &[N],
98        separator_spans: &[Span],
99        start: usize,
100        ctx: &Ctx<'_, 's>,
101        state: &State,
102    ) -> Doc<'s>
103    where
104        N: DocGen<'s> + Spanned,
105    {
106        let mut pos = start;
107        let mut docs = Vec::<Doc<'s>>::with_capacity(list.len() * 2);
108        let mut iter = list.iter().zip_longest(separator_spans.iter()).peekable();
109        while let Some(either_or_both) = iter.next() {
110            match either_or_both {
111                EitherOrBoth::Both(list_item, separator_span) => {
112                    let mut comment_end = None;
113                    let list_item_span = list_item.span();
114                    docs.extend(ctx.end_spaced_comments_without_last_space(
115                        ctx.get_comments_between(pos, list_item_span.start),
116                        &mut comment_end,
117                    ));
118                    if let Some(end) = comment_end {
119                        if ctx.line_bounds.line_distance(end, list_item_span.start) > 0
120                            && ctx.line_bounds.line_distance(pos, end) > 0
121                        {
122                            docs.push(Doc::hard_line());
123                        } else {
124                            docs.push(Doc::soft_line());
125                        }
126                    }
127                    docs.push(list_item.doc(ctx, state));
128                    docs.extend(ctx.start_spaced_comments(
129                        ctx.get_comments_between(list_item_span.end, separator_span.start),
130                    ));
131                    pos = separator_span.end;
132                    if let Some(peeked) = iter.peek() {
133                        docs.push(self.separator.clone());
134                        let mut has_last_line_comment = false;
135                        if let EitherOrBoth::Both(list_item, _) | EitherOrBoth::Left(list_item) =
136                            peeked
137                        {
138                            docs.extend(
139                                ctx.start_spaced_comments_without_last_hard_line(
140                                    ctx.get_comments_between(
141                                        separator_span.end,
142                                        list_item.span().start,
143                                    )
144                                    .take_while(|comment| {
145                                        ctx.line_bounds
146                                            .line_distance(separator_span.end, comment.span.start)
147                                            == 0
148                                    })
149                                    .inspect(|comment| pos = comment.span.end),
150                                    &mut has_last_line_comment,
151                                ),
152                            );
153                        }
154                        if has_last_line_comment {
155                            docs.push(Doc::hard_line());
156                        } else {
157                            docs.push(self.space_after_separator.clone());
158                        }
159                    }
160                }
161                EitherOrBoth::Left(list_item) => {
162                    let mut comment_end = None;
163                    let list_item_span = list_item.span();
164                    docs.extend(ctx.end_spaced_comments_without_last_space(
165                        ctx.get_comments_between(pos, list_item_span.start),
166                        &mut comment_end,
167                    ));
168                    if let Some(end) = comment_end {
169                        if ctx.line_bounds.line_distance(end, list_item_span.start) > 0
170                            && ctx.line_bounds.line_distance(pos, end) > 0
171                        {
172                            docs.push(Doc::hard_line());
173                        } else {
174                            docs.push(Doc::soft_line());
175                        }
176                    }
177                    docs.push(list_item.doc(ctx, state));
178                }
179                EitherOrBoth::Right(..) => unreachable!(),
180            }
181        }
182
183        if self.trailing && ctx.options.trailing_comma {
184            docs.push(Doc::flat_or_break(Doc::nil(), self.separator));
185        }
186        Doc::list(docs)
187    }
188}
189
190/// Only for SCSS/Sass/Less.
191pub(super) fn format_values_list<'s>(
192    values: &[ComponentValue<'s>],
193    comma_spans: Option<&[Span]>,
194    list_span: &Span,
195    ctx: &Ctx<'_, 's>,
196    state: &State,
197) -> Doc<'s> {
198    if let Some(comma_spans) = comma_spans {
199        let doc = SeparatedListFormatter::new(",", Doc::line_or_space())
200            .with_trailing()
201            .format(values, comma_spans, list_span.start, ctx, state)
202            .group();
203        if values.len() == 1 {
204            if ctx.options.trailing_comma {
205                // trailing comma was added by `SeparatedListFormatter` when there're multiple lines
206                doc.append(Doc::flat_or_break(Doc::text(","), Doc::nil()))
207                    .group()
208            } else {
209                doc.append(Doc::text(","))
210            }
211        } else {
212            doc
213        }
214    } else {
215        let mut docs =
216            itertools::intersperse(
217                values.iter().scan(list_span.start, |pos, value| {
218                    let value_span = value.span();
219                    Some(
220                        ctx.end_spaced_comments(ctx.get_comments_between(
221                            mem::replace(pos, value_span.end),
222                            value_span.start,
223                        ))
224                        .chain(iter::once(value.doc(ctx, state)))
225                        .collect::<Vec<_>>()
226                        .into_iter(),
227                    )
228                }),
229                vec![Doc::line_or_space()].into_iter(),
230            )
231            .flatten()
232            .collect::<Vec<_>>();
233
234        if let Some(last) = values.last() {
235            docs.extend(
236                ctx.start_spaced_comments(ctx.get_comments_between(last.span().end, list_span.end)),
237            );
238        }
239
240        Doc::list(docs).group()
241    }
242}
243
244/// Remember to call `.group()` if use this,
245/// otherwise it will always add linebreak.
246pub(super) fn format_operator_prefix_space<'s>(ctx: &Ctx<'_, 's>) -> Doc<'s> {
247    use crate::config::OperatorLineBreak;
248
249    match ctx.options.operator_linebreak {
250        OperatorLineBreak::Before => Doc::line_or_space().nest(ctx.indent_width),
251        OperatorLineBreak::After => Doc::space(),
252    }
253}
254
255/// Remember to call `.group()` if use this,
256/// otherwise it will always add linebreak.
257pub(super) fn format_operator_suffix_space<'s>(ctx: &Ctx<'_, 's>) -> Doc<'s> {
258    use crate::config::OperatorLineBreak;
259
260    match ctx.options.operator_linebreak {
261        OperatorLineBreak::Before => Doc::space(),
262        OperatorLineBreak::After => Doc::line_or_space().nest(ctx.indent_width),
263    }
264}
265
266pub(super) fn format_parenthesized<'s>(
267    body: Doc<'s>,
268    trailing_comments_start: usize,
269    trailing_comments_end: usize,
270    ctx: &Ctx<'_, 's>,
271) -> Doc<'s> {
272    let mut has_last_line_comment = false;
273
274    Doc::text("(")
275        .append(Doc::line_or_nil())
276        .append(body)
277        .concat(ctx.start_spaced_comments_without_last_hard_line(
278            ctx.get_comments_between(trailing_comments_start, trailing_comments_end),
279            &mut has_last_line_comment,
280        ))
281        .nest(ctx.indent_width)
282        .append(if has_last_line_comment {
283            Doc::hard_line()
284        } else {
285            Doc::line_or_nil()
286        })
287        .group()
288        .append(Doc::text(")"))
289}
290
291pub(super) fn format_space_before_block<'s>(
292    previous_end: usize,
293    block_start: usize,
294    ctx: &Ctx<'_, 's>,
295) -> Doc<'s> {
296    if ctx.syntax == Syntax::Sass {
297        let mut has_last_line_comment = false;
298        Doc::list(
299            ctx.start_spaced_comments_without_last_hard_line(
300                ctx.get_comments_between(previous_end, block_start),
301                &mut has_last_line_comment,
302            )
303            .collect(),
304        )
305    } else {
306        Doc::space()
307            .concat(ctx.end_spaced_comments(ctx.get_comments_between(previous_end, block_start)))
308    }
309}
310
311pub(super) fn ident_to_lowercase<'s>(
312    interpolable_ident: &InterpolableIdent<'s>,
313    ctx: &Ctx<'_, 's>,
314    state: &State,
315) -> Doc<'s> {
316    match &interpolable_ident {
317        InterpolableIdent::Literal(ident) if !ident.name.starts_with("--") => {
318            Doc::text(ident.raw.to_ascii_lowercase())
319        }
320        name => name.doc(ctx, state),
321    }
322}
323
324pub(super) fn get_smart_linebreak<N>(
325    start: usize,
326    elements: &[N],
327    prefer_single_line: Option<bool>,
328    ctx: &Ctx<'_, '_>,
329) -> Doc<'static>
330where
331    N: Spanned,
332{
333    let prefer_single_line = prefer_single_line.unwrap_or(ctx.options.prefer_single_line);
334    match elements.first() {
335        Some(element)
336            if !prefer_single_line
337                && ctx.line_bounds.line_distance(start, element.span().start) > 0 =>
338        {
339            Doc::hard_line()
340        }
341        _ => Doc::line_or_space(),
342    }
343}