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 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
190pub(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 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
244pub(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
255pub(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}