Implement dedent proc_macro
This commit is contained in:
commit
57079b4cd3
4 changed files with 224 additions and 0 deletions
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "dedent"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
24
readme.md
Normal file
24
readme.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# dedent
|
||||
|
||||
A procedural macro that removes leading indentation from string literals.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
dedent = { version = "0.1", registry = "nettika" }
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```rust
|
||||
use dedent::dedent;
|
||||
|
||||
let xml = dedent! {r#"
|
||||
<root>
|
||||
<item id="3"/>
|
||||
</root>
|
||||
"#};
|
||||
|
||||
assert_eq!(xml, r#"<root>
|
||||
<item id="3"/>
|
||||
</root>"#);
|
||||
```
|
||||
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()
|
||||
}
|
||||
100
tests/main.rs
Normal file
100
tests/main.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
use dedent::dedent;
|
||||
|
||||
#[test]
|
||||
fn dedent_typical() {
|
||||
assert_eq!(
|
||||
dedent! {"
|
||||
foo
|
||||
|
||||
bar
|
||||
baz
|
||||
"},
|
||||
"foo\n\n bar\nbaz"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedent_typical_parens() {
|
||||
assert_eq!(
|
||||
dedent!(
|
||||
"
|
||||
foo
|
||||
bar
|
||||
|
||||
baz
|
||||
|
||||
"
|
||||
),
|
||||
"foo\n bar\n\nbaz"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedent_whitespace() {
|
||||
assert_eq!(
|
||||
dedent! {"
|
||||
|
||||
|
||||
"},
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedent_trim_trailing() {
|
||||
assert_eq!(
|
||||
dedent! {"
|
||||
trailing:
|
||||
"},
|
||||
"trailing:"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedent_flat() {
|
||||
assert_eq!(dedent! {"foo"}, "foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedent_raw_no_padding() {
|
||||
assert_eq!(
|
||||
dedent! {r"
|
||||
fo\o
|
||||
bar
|
||||
"},
|
||||
"fo\\o\n bar"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedent_raw_with_padding() {
|
||||
assert_eq!(
|
||||
dedent! {r###"
|
||||
fo\o
|
||||
#bar
|
||||
"###},
|
||||
"fo\\o\n #bar"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedent_c_string() {
|
||||
assert_eq!(
|
||||
dedent! {c"
|
||||
foo
|
||||
bar
|
||||
"},
|
||||
c"foo\n bar"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedent_raw_c_string() {
|
||||
assert_eq!(
|
||||
dedent! {cr##"
|
||||
f\oo
|
||||
b#ar
|
||||
"##},
|
||||
c"f\\oo\n b#ar"
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue