Implement dedent proc_macro

This commit is contained in:
Nettika 2025-11-20 01:27:59 -08:00
commit 57079b4cd3
4 changed files with 224 additions and 0 deletions

93
src/lib.rs Normal file
View file

@ -0,0 +1,93 @@
use proc_macro::{TokenStream, TokenTree};
macro_rules! panic {
($msg:expr $(,)?) => {
return stringify!(compile_error!($msg)).parse().unwrap()
};
}
/// Removes leading indentation from string literals.
///
/// # Example
/// ```
/// use dedent::dedent;
///
/// let xml = dedent! {r#"
/// <root>
/// <item id="3"/>
/// </root>
/// "#};
///
/// assert_eq!(xml, r#"<root>
/// <item id="3"/>
/// </root>"#);
/// ```
///
/// Empty lines are ignored when determining indentation, and leading/trailing newlines are trimmed.
#[proc_macro]
pub fn dedent(input: TokenStream) -> TokenStream {
let mut iter = input.into_iter();
let Some(TokenTree::Literal(lit)) = iter.next() else {
panic!("expected a string literal");
};
if iter.next().is_some() {
panic!("expected exactly one string literal");
}
let repr = lit.to_string();
let (is_c, is_raw, bytes) = match &repr.as_bytes() {
[b'c', b'r', rest @ ..] => (true, true, rest),
[b'c', rest @ ..] => (true, false, rest),
[b'r', rest @ ..] => (false, true, rest),
rest @ [b'"', ..] => (false, false, *rest),
_ => panic!("expected a string literal"),
};
let mut hashes_count = 0;
while matches!(bytes[hashes_count as usize], b'#') {
hashes_count += 1;
}
let value = {
let offset = hashes_count as usize;
let start = offset + 1;
let end = bytes.len() - offset - 1;
unsafe { str::from_utf8_unchecked(&bytes[start..end]) }
};
let min_indent = value
.lines()
.filter_map(|line| {
let trimmed_line = line.trim_start_matches(' ');
if trimmed_line.is_empty() {
return None;
}
let indent = line.len() - trimmed_line.len();
Some(indent)
})
.min()
.unwrap_or(0);
let dedented_lines: Vec<&str> = value
.lines()
.map(|line| {
let line = line.trim_end_matches(' ');
if line.is_empty() {
line
} else {
&line[min_indent..]
}
})
.collect();
let new_value = dedented_lines.join("\n").trim_matches('\n').to_string();
let mut prefix = String::new();
let mut suffix = String::new();
if is_c {
prefix.push('c');
}
if is_raw {
prefix.push('r');
}
suffix.push('"');
for _ in 0..hashes_count {
prefix.push('#');
suffix.push('#')
}
prefix.push('"');
format!("{prefix}{new_value}{suffix}").parse().unwrap()
}