typstyle_core/pretty/
comment.rs

1use typst_syntax::{SyntaxKind, SyntaxNode};
2
3use super::{prelude::*, Context, PrettyPrinter};
4
5impl<'a> PrettyPrinter<'a> {
6    pub(super) fn convert_comment(&'a self, _ctx: Context, node: &'a SyntaxNode) -> ArenaDoc<'a> {
7        comment(&self.arena, node)
8    }
9}
10
11enum CommentStyle {
12    Plain,
13    Bullet,
14}
15
16/// Convert either line comment or block comment. Line comments are converted as line suffixes.
17pub fn comment<'a>(arena: &'a Arena<'a>, node: &'a SyntaxNode) -> ArenaDoc<'a> {
18    if node.kind() == SyntaxKind::LineComment {
19        line_comment(arena, node).as_line_suffix()
20    } else if node.kind() == SyntaxKind::BlockComment {
21        block_comment(arena, node)
22    } else {
23        unreachable!("the node should not be a comment node!")
24    }
25}
26
27pub fn line_comment<'a>(arena: &'a Arena<'a>, node: &'a SyntaxNode) -> ArenaDoc<'a> {
28    arena.text(node.text().as_str())
29}
30
31/// It does not add a hardline to the doc.
32pub fn block_comment<'a>(arena: &'a Arena<'a>, node: &'a SyntaxNode) -> ArenaDoc<'a> {
33    // Calculate the number of leading spaces except the first line.
34    let line_num = node.text().lines().count();
35    if line_num == 0 {
36        return arena.text(node.text().as_str());
37    }
38    // Then the comment is multiline.
39    let text = node.text().as_str();
40    let style = get_comment_style(text);
41    match style {
42        CommentStyle::Plain => align_multiline(arena, text),
43        CommentStyle::Bullet => align_multiline_simple(arena, text),
44    }
45}
46
47fn get_comment_style(text: &str) -> CommentStyle {
48    if text
49        .lines()
50        .skip(1)
51        .all(|line| line.trim_start().starts_with('*'))
52    {
53        CommentStyle::Bullet // /*
54    } else {
55        CommentStyle::Plain // otherwise
56    }
57}
58
59/// Get the minimum number of leading spaces in all lines except the first.
60/// Returns None only when the text is a single line.
61fn get_follow_leading(text: &str) -> Option<usize> {
62    text.lines()
63        .skip(1)
64        .map(|line| line.chars().position(|c| c != ' ').unwrap_or(usize::MAX))
65        .min()
66}
67
68/// For general cases. All lines need to be indented together.
69fn align_multiline<'a>(arena: &'a Arena<'a>, text: &'a str) -> ArenaDoc<'a> {
70    let leading = get_follow_leading(text).unwrap();
71    let mut doc = arena.nil();
72    for (i, line) in text.lines().enumerate() {
73        if i == 0 {
74            doc += line;
75        } else {
76            doc += arena.hardline();
77            if line.len() > leading {
78                doc += &line[leading..]; // Remove line prefix
79            } // otherwise this line is blank
80        }
81    }
82    doc.align()
83}
84
85/// For special cases. All lines can be indented independently.
86fn align_multiline_simple<'a>(arena: &'a Arena<'a>, text: &'a str) -> ArenaDoc<'a> {
87    let mut doc = arena.nil();
88    for (i, line) in text.lines().enumerate() {
89        if i > 0 {
90            doc += arena.hardline();
91        }
92        doc += line.trim_start();
93    }
94    doc.nest(1).align()
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_align() {
103        let cmt = "/* 0
104      --- 1
105        -- 2
106    --- 3
107     -- 4 */";
108        let arena = Arena::new();
109        let leading = get_follow_leading(cmt).unwrap();
110        assert_eq!(leading, 4);
111        let doc = arena.text("lorem ipsum") + arena.space() + align_multiline(&arena, cmt);
112        let result = doc.print(80).to_string();
113        // println!("{result}");
114        assert_eq!(
115            result,
116            "lorem ipsum /* 0
117              --- 1
118                -- 2
119            --- 3
120             -- 4 */"
121        );
122    }
123
124    #[test]
125    fn test_align2() {
126        let cmt = "/* 0
127      * 1
128        * 2
129    * 3
130      */";
131        let arena = Arena::new();
132        let doc = arena.text("lorem ipsum") + arena.space() + align_multiline_simple(&arena, cmt);
133        let result = doc.print(80).to_string();
134        // println!("{result}");
135        assert_eq!(
136            result,
137            "lorem ipsum /* 0
138             * 1
139             * 2
140             * 3
141             */"
142        );
143    }
144}