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