leap_lang/
formatter.rs

1use crate::{
2    leaptypes::{Comment, CommentType, LeapEnum, LeapStruct, LeapType, Name, ValueType},
3    parser::{commentsparser, parser::Parser},
4};
5
6#[derive(Debug)]
7struct Block {
8    start: usize,
9    next_start: usize,
10    trail_indent: usize,
11    new_section: bool,
12    text: String,
13}
14
15pub fn format(data: &str) -> Option<String> {
16    let types = Parser::parse(data).ok()?;
17    let mut formatted: Vec<Block> = vec![];
18    for next_type in &types {
19        let mut lines = format_type(next_type);
20        if !lines.is_empty() {
21            if let Some(last) = formatted.last_mut() {
22                last.next_start = lines.last().unwrap().start;
23            }
24            update_trail_indent(&mut lines);
25            formatted.append(&mut lines);
26        }
27    }
28    if let Some(last) = formatted.last_mut() {
29        last.next_start = data.len();
30    }
31    let mut comments = commentsparser::parse(data).into_iter().peekable();
32    let mut result = vec![];
33    for b in formatted {
34        while comments
35            .peek()
36            .map_or(false, |c| c.position.start < b.start)
37        {
38            let indent = if b.new_section { 0 } else { 4 };
39            result.push(format_comment(&comments.next().unwrap(), indent));
40        }
41        let has_trail_comment = comments.peek().map_or(false, |c| match c.comment_type {
42            CommentType::Trail => b.start < c.position.start && c.position.start < b.next_start,
43            _ => false,
44        });
45        if has_trail_comment {
46            let indent = b.trail_indent - b.text.len();
47            let mut text = b.text;
48            text.push_str(&format_comment(&comments.next().unwrap(), indent));
49            result.push(text);
50        } else {
51            result.push(b.text);
52        }
53    }
54    for c in comments {
55        result.push(format_comment(&c, 0));
56    }
57    // remove repeating separators and separators at the start
58    let mut new_result: Vec<String> = vec![];
59    for r in result {
60        if r.is_empty() {
61            // push only if last is not empty and array is not empty
62            let last_separator_or_empty = new_result.last().map(|s| s.is_empty()).unwrap_or(true);
63            if !last_separator_or_empty {
64                new_result.push(r)
65            }
66        } else {
67            new_result.push(r);
68        }
69    }
70    result = new_result;
71    // remove separators at the end
72    if result.last().map(|s| s.is_empty()).unwrap_or(false) {
73        result.pop();
74    }
75    let mut result = result.join("\n");
76    // newline at the end
77    if !result.is_empty() {
78        result.push('\n');
79    }
80    Some(result)
81}
82
83fn update_trail_indent(lines: &mut [Block]) {
84    let max_len = lines.iter().map(|b| b.text.len()).max().unwrap_or(0);
85    let indent = (max_len / 4) * 4 + 4;
86    for b in lines {
87        b.trail_indent = indent;
88    }
89}
90
91fn format_type(leap_type: &LeapType) -> Vec<Block> {
92    let mut lines = vec![];
93    let mut type_lines = match leap_type {
94        LeapType::Struct(s) => format_struct(s),
95        LeapType::Enum(e) => format_enum(e),
96    };
97    lines.append(&mut type_lines);
98    lines
99}
100
101fn format_struct(leap_struct: &LeapStruct) -> Vec<Block> {
102    let mut lines = vec![];
103    let text = format!(
104        ".struct {}{}",
105        leap_struct.name.get(),
106        format_type_args(&leap_struct.args)
107    );
108    let next_start = if let Some(last) = leap_struct.props.last() {
109        last.position.start
110    } else {
111        leap_struct.position.end()
112    };
113    lines.push(Block {
114        start: leap_struct.position.start,
115        next_start,
116        trail_indent: 0,
117        new_section: true,
118        text,
119    });
120    for i in 0..leap_struct.props.len() {
121        let prop = &leap_struct.props[i];
122        let next_prop = leap_struct.props.get(i + 1);
123        let text = format!(
124            "    {}: {}",
125            prop.name.get(),
126            format_prop_type(&prop.prop_type)
127        );
128        let next_start = if let Some(next) = next_prop {
129            next.position.start
130        } else {
131            prop.position.end()
132        };
133        lines.push(Block {
134            start: prop.position.start,
135            next_start,
136            trail_indent: 0,
137            new_section: false,
138            text,
139        })
140    }
141    lines
142}
143
144fn format_enum(leap_enum: &LeapEnum) -> Vec<Block> {
145    let mut lines = vec![];
146    let text = format!(
147        ".enum {}{}",
148        leap_enum.name.get(),
149        format_type_args(&leap_enum.args)
150    );
151    let next_start = if let Some(last) = leap_enum.variants.last() {
152        last.position.start
153    } else {
154        leap_enum.position.end()
155    };
156    lines.push(Block {
157        start: leap_enum.position.start,
158        next_start,
159        trail_indent: 0,
160        new_section: true,
161        text,
162    });
163    for i in 0..leap_enum.variants.len() {
164        let variant = &leap_enum.variants[i];
165        let next_var = leap_enum.variants.get(i + 1);
166        let text = if variant.name.get() == variant.prop_type.name() {
167            format!("    {}", format_prop_type(&variant.prop_type))
168        } else {
169            format!(
170                "    {}: {}",
171                variant.name.get(),
172                format_prop_type(&variant.prop_type)
173            )
174        };
175        let next_start = if let Some(next) = next_var {
176            next.position.start
177        } else {
178            variant.position.end()
179        };
180        lines.push(Block {
181            start: variant.position.start,
182            next_start,
183            trail_indent: 0,
184            new_section: false,
185            text,
186        })
187    }
188    lines
189}
190
191fn format_type_args(args: &[Name]) -> String {
192    if args.is_empty() {
193        "".to_owned()
194    } else {
195        let mut tokens = vec![];
196        for name in args {
197            tokens.push(name.get());
198        }
199        let tokens = tokens.join(" ");
200        format!("[{}]", tokens)
201    }
202}
203
204fn format_prop_type(prop_type: &ValueType) -> String {
205    match prop_type {
206        ValueType::Simple(t) => t.name(),
207        ValueType::List(t) => format!("list[{}]", format_prop_type(t)),
208        ValueType::TypeArg(n) => n.get().to_owned(),
209        ValueType::LeapType { name, args } => {
210            if args.is_empty() {
211                name.get().to_owned()
212            } else {
213                let args = args
214                    .iter()
215                    .map(format_prop_type)
216                    .collect::<Vec<_>>()
217                    .join(" ");
218                format!("{}[{}]", name.get(), args)
219            }
220        }
221    }
222}
223
224fn format_comment(comment: &Comment, indent: usize) -> String {
225    let indent = String::from_utf8(vec![b' '; indent]).unwrap();
226    match comment.comment_type {
227        CommentType::Line | CommentType::Trail => format!("{}/-- {}", indent, comment.comment),
228        CommentType::Separator => "".to_owned(),
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_update_trail_indent() {
238        let mut blocks = vec![Block {
239            start: 0,
240            next_start: 0,
241            trail_indent: 0,
242            new_section: false,
243            text: "aaaa".to_owned(),
244        }];
245        update_trail_indent(&mut blocks);
246        assert_eq!(blocks[0].trail_indent, 8);
247        let mut blocks = vec![Block {
248            start: 0,
249            next_start: 0,
250            trail_indent: 0,
251            new_section: false,
252            text: "aaaaaaa".to_owned(),
253        }];
254        update_trail_indent(&mut blocks);
255        assert_eq!(blocks[0].trail_indent, 8);
256    }
257
258    #[test]
259    fn test_format() {
260        assert_eq!(format(".struct    s1").unwrap(), ".struct s1\n");
261        assert_eq!(
262            format(".struct    s1 [  a   b   ]").unwrap(),
263            ".struct s1[a b]\n"
264        );
265        assert_eq!(
266            format(".struct    s1  a:   int").unwrap(),
267            ".struct s1\n    a: int\n"
268        );
269        assert_eq!(format(".enum    e1    s1").unwrap(), ".enum e1\n    s1\n");
270        assert_eq!(
271            format(".enum    e1    aaa:    s1").unwrap(),
272            ".enum e1\n    aaa: s1\n"
273        );
274        assert_eq!(
275            format(".enum    e1[a   b]    s1  v2[ a   b ]").unwrap(),
276            ".enum e1[a b]\n    s1\n    v2[a b]\n"
277        );
278    }
279
280    #[test]
281    fn test_format_with_comments() {
282        assert_eq!(format("").unwrap(), "");
283        assert_eq!(
284            format("/ text\n.struct    s1").unwrap(),
285            "/-- text\n.struct s1\n"
286        );
287        assert_eq!(
288            format("/- text\n.struct    s1").unwrap(),
289            "/-- text\n.struct s1\n"
290        );
291        assert_eq!(
292            format("/-- text\n.struct    s1").unwrap(),
293            "/-- text\n.struct s1\n"
294        );
295        assert_eq!(
296            format("/--- text\n.struct    s1").unwrap(),
297            "/-- - text\n.struct s1\n"
298        );
299        assert_eq!(
300            format("/-- -- text\n.struct    s1").unwrap(),
301            "/-- -- text\n.struct s1\n"
302        );
303        assert_eq!(
304            format("/ text\n\n.struct    s1").unwrap(),
305            "/-- text\n\n.struct s1\n"
306        );
307        assert_eq!(
308            format("\n\n\n\n.struct  \n\n\n\n  s1\n\n\n\n").unwrap(),
309            ".struct s1\n"
310        );
311        assert_eq!(
312            format("/ text\n\n\n.struct    s1").unwrap(),
313            "/-- text\n\n.struct s1\n"
314        );
315        assert_eq!(
316            format("/ text     \n\n\n.struct    s1").unwrap(),
317            "/-- text\n\n.struct s1\n"
318        );
319
320        assert_eq!(
321            format(".struct s1 / text").unwrap(),
322            ".struct s1  /-- text\n"
323        );
324        assert_eq!(format(".enum s1 / text").unwrap(), ".enum s1    /-- text\n");
325
326        assert_eq!(
327            format(".struct s1\n/ text").unwrap(),
328            ".struct s1\n/-- text\n"
329        );
330        assert_eq!(format(".enum s1\n/ text").unwrap(), ".enum s1\n/-- text\n");
331
332        assert_eq!(
333            format(".struct s1\nv: int / text").unwrap(),
334            ".struct s1\n    v: int  /-- text\n"
335        );
336        assert_eq!(
337            format(".enum s1\nval / text").unwrap(),
338            ".enum s1\n    val     /-- text\n"
339        );
340
341        assert_eq!(
342            format(".struct s1\n/ text\nv: int").unwrap(),
343            ".struct s1\n    /-- text\n    v: int\n"
344        );
345        assert_eq!(
346            format(".enum aaaaaa\n/ text\nval").unwrap(),
347            ".enum aaaaaa\n    /-- text\n    val\n"
348        );
349
350        assert_eq!(
351            format(".struct s1\n/ text\n\n\n/ text\nv: int").unwrap(),
352            ".struct s1\n    /-- text\n\n    /-- text\n    v: int\n"
353        );
354    }
355
356    #[test]
357    fn test_format_complex() {
358        let formatted = format(
359            "
360        / text1 text
361        / tttt2
362
363        /text3
364
365        .struct some-my-struct / text4
366        /text5
367
368        /text6
369        /text7
370        v1: list[int] /text8
371        v2: list[list[int]]
372
373        /text9
374        /text10
375
376        .enum value-enum
377
378        / test11
379
380        val1
381        val2
382
383        / text12
384        ",
385        )
386        .unwrap();
387        let expected = "/-- text1 text
388/-- tttt2
389
390/-- text3
391
392.struct some-my-struct  /-- text4
393    /-- text5
394
395    /-- text6
396    /-- text7
397    v1: list[int]       /-- text8
398    v2: list[list[int]]
399
400/-- text9
401/-- text10
402
403.enum value-enum
404
405    /-- test11
406
407    val1
408    val2
409
410/-- text12
411";
412        assert_eq!(formatted, expected);
413        // if format formatted, result should be same
414        assert_eq!(format(&formatted).unwrap(), expected);
415    }
416}