string_literal_const_replace/
lib.rs

1//! A macro for compile-time replacement on string literals.
2//!
3//! This is meant for combining with other macros, such as doing a find/replace on a `stringify!`
4//! result in a const context.
5//!
6//! See [`string_literal_replace!`].
7
8#![feature(proc_macro_expand)]
9
10use proc_macro::{Literal, Span, TokenStream, TokenTree};
11
12/// Replace contents of a string literal at compile-time.
13///
14/// ```
15/// # use string_literal_const_replace::string_literal_replace;
16/// assert_eq!(string_literal_replace!("hello, world!" ("hello" -> "goodbye")), "goodbye, world!");
17/// ```
18///
19/// This macro can chain with other macros that output string literals, like [`concat!`]:
20///
21/// ```
22/// # use string_literal_const_replace::string_literal_replace;
23/// assert_eq!(
24///     string_literal_replace!(concat!("hello", ", world!") ("hello" -> "goodbye")),
25///     "goodbye, world!"
26/// );
27/// ```
28#[proc_macro]
29pub fn string_literal_replace(input: TokenStream) -> TokenStream {
30    let ParsedInput {
31        original_string,
32        replacements,
33    } = syn::parse_macro_input!(input as ParsedInput);
34    let mut processed_str = original_string.clone();
35    for (from, to) in replacements {
36        processed_str = processed_str.replace(&from, &to);
37    }
38    let mut lit = Literal::string(&processed_str);
39    lit.set_span(Span::call_site());
40    TokenTree::Literal(lit).into()
41}
42
43/// The macro input, parsed.
44struct ParsedInput {
45    /// The original, pre-replacements string.
46    original_string: String,
47    /// The replacements being used.
48    replacements: Vec<(String, String)>,
49}
50
51impl syn::parse::Parse for ParsedInput {
52    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
53        let original_string = input.parse::<StringExpander>()?.into();
54        let mut replacements = Vec::new();
55        while let Ok(group) = input.parse::<proc_macro2::Group>() {
56            let Replacement { from, to } = syn::parse2(group.stream())?;
57            replacements.push((from, to));
58        }
59        Ok(Self {
60            original_string,
61            replacements,
62        })
63    }
64}
65
66/// A single replacement, parsed.
67struct Replacement {
68    /// The text to search for.
69    from: String,
70    /// The text to replace with.
71    to: String,
72}
73impl syn::parse::Parse for Replacement {
74    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
75        let from = input.parse::<StringExpander>()?.into();
76        input.parse::<syn::Token![->]>()?;
77        let to = input.parse::<StringExpander>()?.into();
78        Ok(Self { from, to })
79    }
80}
81
82/// A single string literal, parsed and maybe expanded.
83struct StringExpander {
84    /// The parsed and maybe expanded string literal.
85    expanded: String,
86}
87impl From<StringExpander> for String {
88    fn from(value: StringExpander) -> Self {
89        value.expanded
90    }
91}
92impl syn::parse::Parse for StringExpander {
93    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
94        use quote::ToTokens;
95
96        if let Ok(expr) = input.parse::<syn::ExprMacro>() {
97            let expression = TokenStream::from(expr.into_token_stream());
98            Ok(Self {
99                expanded: syn::parse::<syn::LitStr>(
100                    expression.expand_expr().map_err(|e| input.error(e))?,
101                )?
102                .value(),
103            })
104        } else {
105            Ok(Self {
106                expanded: input.parse::<syn::LitStr>()?.value(),
107            })
108        }
109    }
110}