codize/
block.rs

1use crate::{Code, Concat, Format, FormatCode};
2
3/// A block of code with a starting line, ending line, and an indented body
4#[derive(derivative::Derivative)]
5#[derivative(Debug, Clone, PartialEq)]
6pub struct Block {
7    /// If this block should be connected to the end of a previous block
8    /// (for example, `else {`)
9    pub connect: bool,
10    /// The start of the block (for example, `if (x) {`)
11    pub start: String,
12    /// The end of the block (for example, `}`)
13    pub end: String,
14    /// The body of the block. Usually the body is the part that gets indented
15    concat_body: Concat,
16    /// When to inline
17    #[derivative(Debug = "ignore", PartialEq = "ignore")]
18    inline_condition: Option<fn(&Block) -> bool>,
19}
20
21impl Block {
22    /// Create a new block with empty body
23    pub fn empty<TStart, TEnd>(start: TStart, end: TEnd) -> Self
24    where
25        TStart: ToString,
26        TEnd: ToString,
27    {
28        Self {
29            connect: false,
30            start: start.to_string(),
31            concat_body: Concat::empty(),
32            end: end.to_string(),
33            inline_condition: None,
34        }
35    }
36
37    /// Create a new code block
38    pub fn new<TStart, TBody, TEnd>(start: TStart, body: TBody, end: TEnd) -> Self
39    where
40        TStart: ToString,
41        TEnd: ToString,
42        TBody: IntoIterator,
43        TBody::Item: Into<Code>,
44    {
45        Self {
46            connect: false,
47            start: start.to_string(),
48            concat_body: Concat::new(body),
49            end: end.to_string(),
50            inline_condition: None,
51        }
52    }
53
54    /// Set this block to start on the same line as the end of the previous block
55    pub fn connected(mut self) -> Self {
56        self.connect = true;
57        self
58    }
59
60    /// Set a condition for displaying the block as one line
61    pub fn inline_when(mut self, condition: fn(&Block) -> bool) -> Self {
62        self.inline_condition = Some(condition);
63        self
64    }
65
66    /// Set the inline condition to be always true
67    pub fn inlined(mut self) -> Self {
68        self.inline_condition = Some(|_| true);
69        self
70    }
71
72    /// Set the inline condition to be always false
73    pub fn never_inlined(mut self) -> Self {
74        self.inline_condition = Some(|_| false);
75        self
76    }
77
78    /// Get the body of the block
79    #[inline]
80    pub fn body(&self) -> &[Code] {
81        &self.concat_body
82    }
83
84    /// Should the block be displayed in one line
85    pub fn should_inline(&self) -> bool {
86        if let Some(condition) = self.inline_condition {
87            condition(self)
88        } else {
89            self.should_inline_intrinsic()
90        }
91    }
92
93    /// Should intrinsicly inline the block
94    ///
95    /// This is used for blocks that only contain one line of code
96    pub fn should_inline_intrinsic(&self) -> bool {
97        self.body().len() == 1 && self.body()[0].should_inline()
98    }
99}
100
101impl From<Block> for Code {
102    fn from(x: Block) -> Self {
103        Code::Block(Box::new(x))
104    }
105}
106
107impl std::fmt::Display for Block {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        write!(f, "{}", self.format())
110    }
111}
112
113impl FormatCode for Block {
114    fn size_hint(&self) -> usize {
115        // add the body, start, and end
116        self.concat_body.size_hint() + 2
117    }
118
119    fn format_into_vec_with(
120        &self,
121        format: &Format,
122        out: &mut Vec<String>,
123        connect: bool,
124        indent: &str,
125    ) {
126        let connect = self.connect || connect;
127        crate::append_line(out, &self.start, connect, indent);
128        let should_inline = self.should_inline();
129
130        if should_inline {
131            for code in self.body() {
132                code.format_into_vec_with(format, out, true, indent);
133            }
134        } else {
135            // indent the body
136            let i = format.indent;
137            let new_indent = if i < 0 {
138                format!("\t{indent}")
139            } else {
140                let i = i as usize;
141                format!("{:i$}{indent}", "")
142            };
143            for code in self.body() {
144                code.format_into_vec_with(format, out, false, &new_indent);
145            }
146        }
147        crate::append_line(out, &self.end, should_inline, indent);
148    }
149}
150
151/// Macro for creating [`Block`]s
152///
153/// # Examples
154///
155/// ```
156/// use codize::cblock;
157///
158/// let expected =
159/// "fn main() {
160///     foo();
161/// }";
162///
163/// let code = cblock!("fn main() {", [
164///    "foo();",
165/// ], "}");
166/// assert_eq!(expected, code.to_string());
167///
168/// ```
169///
170/// Anything that implements `Into<Code>` can be used in the body.
171/// It can also be anything that implements `IntoIterator` and returns `Into<Code>`.
172///
173/// You can call [`Block::connected`] to connect the start of the block with the end of the last block,
174/// such as an `else` block.
175/// ```
176/// use codize::cblock;
177///
178/// let expected =
179/// "fn foo(y: bool) {
180///     if (x()) {
181///         bar();
182///     } else if (y) {
183///         baz();
184///     }
185/// }";
186///
187/// let func = "x";
188/// let code = cblock!("fn foo(y: bool) {", [
189///    cblock!(format!("if ({func}()) {{"), [
190///       "bar();",
191///    ], "}"),
192///    cblock!("else if (y) {", [
193///       "baz();"
194///    ], "}").connected(),
195/// ], "}");
196/// assert_eq!(expected, code.to_string());
197///
198/// ```
199#[macro_export]
200macro_rules! cblock {
201    ($start:expr, [] , $end:expr) => {
202        $crate::Block::empty($start, $end)
203    };
204    ($start:expr, [ $( $body:expr ),* $(,)? ] , $end:expr) => {
205        $crate::Block::new($start, [ $($crate::Code::from($body)),* ], $end)
206    };
207    ($start:expr, $body:expr, $end:expr) => {
208        $crate::Block::new($start, $body, $end)
209    };
210}
211
212#[cfg(test)]
213mod test {
214    use indoc::indoc;
215
216    #[test]
217    fn empty() {
218        let code = cblock!("", [], "");
219        // start and end on separate lines
220        assert_eq!("\n", code.to_string());
221    }
222
223    #[test]
224    fn empty_body() {
225        let code = cblock!("fn main() {", [], "}");
226        assert_eq!("fn main() {\n}", code.to_string());
227    }
228
229    #[test]
230    fn different_types() {
231        let code = cblock!(
232            "fn main() {",
233            [
234                "foo",
235                "bar".to_string(),
236                cblock!("if (x) {", ["baz", "qux".to_string(),], "}"),
237            ],
238            "}"
239        );
240        let expected = indoc! {"
241            fn main() {
242                foo
243                bar
244                if (x) {
245                    baz
246                    qux
247                }
248            }"};
249        assert_eq!(expected, code.to_string());
250    }
251
252    #[test]
253    fn iteratable() {
254        let body = vec![
255            cblock!("if (x()) {", ["bar();"], "}"),
256            cblock!("else if (y) {", ["baz();"], "}").connected(),
257        ];
258        let code = cblock!("fn foo(y: bool) {", body, "}");
259        let expected = indoc! {"
260            fn foo(y: bool) {
261                if (x()) {
262                    bar();
263                } else if (y) {
264                    baz();
265                }
266            }"};
267        assert_eq!(expected, code.to_string());
268    }
269
270    fn is_one_thing(block: &crate::Block) -> bool {
271        block.body().len() == 1
272    }
273
274    #[test]
275    fn inline_condition() {
276        let body = vec![
277            cblock!("if (x()) {", ["bar();"], "}").inline_when(is_one_thing),
278            cblock!("else if (y) {", ["baz();", "baz();"], "}")
279                .connected()
280                .inline_when(is_one_thing),
281        ];
282        let code = cblock!("fn foo(y: bool) {", body, "}");
283        let expected = indoc! {"
284            fn foo(y: bool) {
285                if (x()) { bar(); } else if (y) {
286                    baz();
287                    baz();
288                }
289            }"};
290        assert_eq!(expected, code.to_string());
291    }
292
293    #[test]
294    fn no_end_chaining() {
295        let code = cblock! {
296            "{",
297            [
298                "",
299                cblock!{
300                    "if xxx:",
301                    ["foo()"],
302                    ""
303                }.connected(),
304                cblock!{
305                    "elif yyy:",
306                    ["bar()"],
307                    ""
308                }.connected(),
309            ],
310            "}"
311        }
312        .never_inlined();
313        let expected = indoc! {"
314            {
315                if xxx:
316                    foo()
317                elif yyy:
318                    bar()
319
320            }"};
321        assert_eq!(expected, code.to_string());
322    }
323}