1use crate::ctx::Ctx;
2use raffia::{
3 token::{Comment, CommentKind},
4 Syntax,
5};
6use tiny_pretty::Doc;
7
8pub(crate) fn format_comment<'s>(comment: &Comment<'s>, ctx: &Ctx<'_, 's>) -> Doc<'s> {
9 match comment.kind {
10 CommentKind::Block => {
11 let mut docs = vec![Doc::text("/*")];
12 if ctx.options.format_comments
13 && !comment
14 .content
15 .as_bytes()
16 .first()
17 .map(|b| b.is_ascii_whitespace())
18 .unwrap_or(true)
19 {
20 docs.push(Doc::space());
21 }
22
23 let mut lines = comment
25 .content
26 .split('\n')
27 .map(|s| s.strip_suffix('\r').unwrap_or(s));
28
29 let is_jsdoc_like = lines.clone().skip(1).all(|line| {
30 let trimmed = line.trim_start();
31 trimmed.is_empty() || trimmed.starts_with('*')
32 });
33
34 if is_jsdoc_like {
35 if let Some(first) = lines.next() {
36 docs.push(Doc::text(first));
37 };
38 docs.extend(
39 lines.map(|line| Doc::hard_line().append(Doc::text(line.trim_start()))),
40 );
41 } else if ctx.options.align_comments {
42 docs.append(&mut reflow(comment, ctx));
43 } else {
44 docs.extend(itertools::intersperse(
45 lines.map(Doc::text),
46 Doc::empty_line(),
47 ));
48 }
49
50 if ctx.options.format_comments
51 && !comment
52 .content
53 .as_bytes()
54 .last()
55 .map(|b| b.is_ascii_whitespace())
56 .unwrap_or(true)
57 {
58 docs.push(Doc::space());
59 }
60 docs.push(Doc::text("*/"));
61
62 if is_jsdoc_like {
63 Doc::list(docs).nest(1)
64 } else {
65 Doc::list(docs)
66 }
67 }
68 CommentKind::Line => {
69 let content = comment.content.trim_end();
70 if ctx.options.format_comments {
71 let (is_doc_comment, content) = match (ctx.syntax, content.strip_prefix('/')) {
72 (Syntax::Scss | Syntax::Sass, Some(content)) => (true, content),
73 _ => (false, content),
74 };
75 let prefix = if is_doc_comment { "///" } else { "//" };
76 if content
77 .as_bytes()
78 .first()
79 .is_none_or(|b| b.is_ascii_whitespace())
80 {
81 Doc::text(format!("{prefix}{content}"))
82 } else {
83 Doc::text(format!("{prefix} {content}",))
84 }
85 } else {
86 Doc::text(format!("//{content}"))
87 }
88 }
89 }
90}
91
92pub(super) fn reflow<'s>(comment: &Comment<'s>, ctx: &Ctx<'_, 's>) -> Vec<Doc<'s>> {
93 let col = comment
94 .content
95 .lines()
96 .skip(1)
97 .filter(|line| !line.trim().is_empty())
98 .map(|line| {
99 line.as_bytes()
100 .iter()
101 .take_while(|byte| byte.is_ascii_whitespace())
102 .count()
103 })
104 .min()
105 .unwrap_or_default()
106 .min(
107 ctx.line_bounds
108 .get_line_col(comment.span.start)
109 .1
110 .saturating_sub(1),
111 );
112 let mut docs = Vec::with_capacity(2);
113 let mut lines = comment.content.split('\n').enumerate().peekable();
114 while let Some((i, line)) = lines.next() {
115 let s = line.strip_suffix('\r').unwrap_or(line);
116 let s = if s.starts_with([' ', '\t']) && i > 0 {
117 s.get(col..).unwrap_or(s)
118 } else {
119 s
120 };
121 if i > 0 {
122 if s.trim().is_empty() && lines.peek().is_some() {
123 docs.push(Doc::empty_line());
124 } else {
125 docs.push(Doc::hard_line());
126 }
127 }
128 docs.push(Doc::text(s));
129 }
130 docs
131}