Implement dedent proc_macro
This commit is contained in:
commit
57079b4cd3
4 changed files with 224 additions and 0 deletions
93
src/lib.rs
Normal file
93
src/lib.rs
Normal 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()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue