str_block/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use proc_macro::TokenStream;
4use proclet::{op, pm1::StringLiteral, prelude::*, proclet, punctuated, Optional};
5
6/// Remove the first line if it's empty except for whitespace, and remove common indentation from
7/// all lines, if any. Lines that are empty except for whitespace are treated as if they have the
8/// common indentation.
9///
10/// Call with `{}` like `str_block!{"string"}` to stop rustfmt from moving your string.
11#[proc_macro]
12pub fn str_block(input: TokenStream) -> TokenStream {
13    proclet(input, |input| {
14        let strings = punctuated(StringLiteral::parser(), Optional(op(","))).parse_all(input)?;
15
16        // concat input
17        let str: String = strings.into_iter().map(|(s, _)| s.into_value()).collect();
18
19        let mut lines = str.lines();
20        let mut lines2 = lines.clone();
21        let Some(first) = lines.next() else {
22            // input was an empty string
23            return Ok(StringLiteral::new(String::new()));
24        };
25        // skip first line if it's empty
26        let first = if first.trim().is_empty() {
27            let _ = lines2.next();
28            if let Some(second) = lines.next() {
29                second
30            } else {
31                // input was one line of only whitespace
32                return Ok(StringLiteral::new(String::new()));
33            }
34        } else {
35            first
36        };
37
38        let first_trimmed = first.trim_start();
39        let mut prefix = &first[..first.len() - first_trimmed.len()];
40        if !prefix.is_empty() {
41            for line in lines {
42                if !line.trim().is_empty() {
43                    let ci = prefix
44                        .chars()
45                        .zip(line.chars())
46                        .take_while(|(p, l)| p == l)
47                        .fold(0, |ci, (p, _)| ci + p.len_utf8());
48                    if ci < prefix.len() {
49                        prefix = &prefix[..ci];
50                        if prefix.is_empty() {
51                            break;
52                        }
53                    }
54                }
55            }
56        }
57
58        let mut output = String::from(lines2.next().unwrap().strip_prefix(prefix).unwrap_or(""));
59        for line in lines2 {
60            output.push('\n');
61            output.push_str(line.strip_prefix(prefix).unwrap_or(""));
62        }
63        if str.ends_with('\n') {
64            output.push('\n');
65        }
66        Ok(StringLiteral::new(output))
67    })
68}