malva/doc_gen/
comment.rs

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            // we don't use `str::lines()` since it uses `split_inclusive`
24            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}