indoc/
lib.rs

1//! [![github]](https://github.com/dtolnay/indoc) [![crates-io]](https://crates.io/crates/indoc) [![docs-rs]](https://docs.rs/indoc)
2//!
3//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
4//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
5//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
6//!
7//! <br>
8//!
9//! This crate provides a procedural macro for indented string literals. The
10//! `indoc!()` macro takes a multiline string literal and un-indents it at
11//! compile time so the leftmost non-space character is in the first column.
12//!
13//! ```toml
14//! [dependencies]
15//! indoc = "2"
16//! ```
17//!
18//! <br>
19//!
20//! # Using indoc
21//!
22//! ```
23//! use indoc::indoc;
24//!
25//! fn main() {
26//!     let testing = indoc! {"
27//!         def hello():
28//!             print('Hello, world!')
29//!
30//!         hello()
31//!     "};
32//!     let expected = "def hello():\n    print('Hello, world!')\n\nhello()\n";
33//!     assert_eq!(testing, expected);
34//! }
35//! ```
36//!
37//! Indoc also works with raw string literals:
38//!
39//! ```
40//! use indoc::indoc;
41//!
42//! fn main() {
43//!     let testing = indoc! {r#"
44//!         def hello():
45//!             print("Hello, world!")
46//!
47//!         hello()
48//!     "#};
49//!     let expected = "def hello():\n    print(\"Hello, world!\")\n\nhello()\n";
50//!     assert_eq!(testing, expected);
51//! }
52//! ```
53//!
54//! And byte string literals:
55//!
56//! ```
57//! use indoc::indoc;
58//!
59//! fn main() {
60//!     let testing = indoc! {b"
61//!         def hello():
62//!             print('Hello, world!')
63//!
64//!         hello()
65//!     "};
66//!     let expected = b"def hello():\n    print('Hello, world!')\n\nhello()\n";
67//!     assert_eq!(testing[..], expected[..]);
68//! }
69//! ```
70//!
71//! <br><br>
72//!
73//! # Formatting macros
74//!
75//! The indoc crate exports five additional macros to substitute conveniently
76//! for the standard library's formatting macros:
77//!
78//! - `formatdoc!($fmt, ...)`&ensp;&mdash;&ensp;equivalent to `format!(indoc!($fmt), ...)`
79//! - `printdoc!($fmt, ...)`&ensp;&mdash;&ensp;equivalent to `print!(indoc!($fmt), ...)`
80//! - `eprintdoc!($fmt, ...)`&ensp;&mdash;&ensp;equivalent to `eprint!(indoc!($fmt), ...)`
81//! - `writedoc!($dest, $fmt, ...)`&ensp;&mdash;&ensp;equivalent to `write!($dest, indoc!($fmt), ...)`
82//! - `concatdoc!(...)`&ensp;&mdash;&ensp;equivalent to `concat!(...)` with each string literal wrapped in `indoc!`
83//!
84//! ```
85//! # macro_rules! env {
86//! #     ($var:literal) => {
87//! #         "example"
88//! #     };
89//! # }
90//! #
91//! use indoc::{concatdoc, printdoc};
92//!
93//! const HELP: &str = concatdoc! {"
94//!     Usage: ", env!("CARGO_BIN_NAME"), " [options]
95//!
96//!     Options:
97//!         -h, --help
98//! "};
99//!
100//! fn main() {
101//!     printdoc! {"
102//!         GET {url}
103//!         Accept: {mime}
104//!         ",
105//!         url = "http://localhost:8080",
106//!         mime = "application/json",
107//!     }
108//! }
109//! ```
110//!
111//! <br><br>
112//!
113//! # Explanation
114//!
115//! The following rules characterize the behavior of the `indoc!()` macro:
116//!
117//! 1. Count the leading spaces of each line, ignoring the first line and any
118//!    lines that are empty or contain spaces only.
119//! 2. Take the minimum.
120//! 3. If the first line is empty i.e. the string begins with a newline, remove
121//!    the first line.
122//! 4. Remove the computed number of spaces from the beginning of each line.
123
124#![doc(html_root_url = "https://docs.rs/indoc/2.0.7")]
125#![allow(
126    clippy::derive_partial_eq_without_eq,
127    clippy::from_iter_instead_of_collect,
128    clippy::module_name_repetitions,
129    clippy::needless_doctest_main,
130    clippy::needless_pass_by_value,
131    clippy::trivially_copy_pass_by_ref,
132    clippy::type_complexity
133)]
134
135mod error;
136mod expr;
137#[allow(dead_code)]
138mod unindent;
139
140use crate::error::{Error, Result};
141use crate::unindent::do_unindent;
142use proc_macro::token_stream::IntoIter as TokenIter;
143use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
144use std::iter::{self, Peekable};
145use std::str::FromStr;
146
147#[derive(#[automatically_derived]
impl ::core::marker::Copy for Macro { }Copy, #[automatically_derived]
impl ::core::clone::Clone for Macro {
    #[inline]
    fn clone(&self) -> Macro { *self }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for Macro {
    #[inline]
    fn eq(&self, other: &Macro) -> bool {
        let __self_discr = ::core::intrinsics::discriminant_value(self);
        let __arg1_discr = ::core::intrinsics::discriminant_value(other);
        __self_discr == __arg1_discr
    }
}PartialEq)]
148enum Macro {
149    Indoc,
150    Format,
151    Print,
152    Eprint,
153    Write,
154    Concat,
155}
156
157/// Unindent and produce `&'static str` or `&'static [u8]`.
158///
159/// Supports normal strings, raw strings, bytestrings, and raw bytestrings.
160///
161/// # Example
162///
163/// ```
164/// # use indoc::indoc;
165/// #
166/// // The type of `program` is &'static str
167/// let program = indoc! {"
168///     def hello():
169///         print('Hello, world!')
170///
171///     hello()
172/// "};
173/// print!("{}", program);
174/// ```
175///
176/// ```text
177/// def hello():
178///     print('Hello, world!')
179///
180/// hello()
181/// ```
182#[proc_macro]
183pub fn indoc(input: TokenStream) -> TokenStream {
184    expand(input, Macro::Indoc)
185}
186
187/// Unindent and call `format!`.
188///
189/// Argument syntax is the same as for [`std::format!`].
190///
191/// # Example
192///
193/// ```
194/// # use indoc::formatdoc;
195/// #
196/// let request = formatdoc! {"
197///     GET {url}
198///     Accept: {mime}
199///     ",
200///     url = "http://localhost:8080",
201///     mime = "application/json",
202/// };
203/// println!("{}", request);
204/// ```
205///
206/// ```text
207/// GET http://localhost:8080
208/// Accept: application/json
209/// ```
210#[proc_macro]
211pub fn formatdoc(input: TokenStream) -> TokenStream {
212    expand(input, Macro::Format)
213}
214
215/// Unindent and call `print!`.
216///
217/// Argument syntax is the same as for [`std::print!`].
218///
219/// # Example
220///
221/// ```
222/// # use indoc::printdoc;
223/// #
224/// printdoc! {"
225///     GET {url}
226///     Accept: {mime}
227///     ",
228///     url = "http://localhost:8080",
229///     mime = "application/json",
230/// }
231/// ```
232///
233/// ```text
234/// GET http://localhost:8080
235/// Accept: application/json
236/// ```
237#[proc_macro]
238pub fn printdoc(input: TokenStream) -> TokenStream {
239    expand(input, Macro::Print)
240}
241
242/// Unindent and call `eprint!`.
243///
244/// Argument syntax is the same as for [`std::eprint!`].
245///
246/// # Example
247///
248/// ```
249/// # use indoc::eprintdoc;
250/// #
251/// eprintdoc! {"
252///     GET {url}
253///     Accept: {mime}
254///     ",
255///     url = "http://localhost:8080",
256///     mime = "application/json",
257/// }
258/// ```
259///
260/// ```text
261/// GET http://localhost:8080
262/// Accept: application/json
263/// ```
264#[proc_macro]
265pub fn eprintdoc(input: TokenStream) -> TokenStream {
266    expand(input, Macro::Eprint)
267}
268
269/// Unindent and call `write!`.
270///
271/// Argument syntax is the same as for [`std::write!`].
272///
273/// # Example
274///
275/// ```
276/// # use indoc::writedoc;
277/// # use std::io::Write;
278/// #
279/// let _ = writedoc!(
280///     std::io::stdout(),
281///     "
282///         GET {url}
283///         Accept: {mime}
284///     ",
285///     url = "http://localhost:8080",
286///     mime = "application/json",
287/// );
288/// ```
289///
290/// ```text
291/// GET http://localhost:8080
292/// Accept: application/json
293/// ```
294#[proc_macro]
295pub fn writedoc(input: TokenStream) -> TokenStream {
296    expand(input, Macro::Write)
297}
298
299/// Unindent and call `concat!`.
300///
301/// Argument syntax is the same as for [`std::concat!`].
302///
303/// # Example
304///
305/// ```
306/// # use indoc::concatdoc;
307/// #
308/// # macro_rules! env {
309/// #     ($var:literal) => {
310/// #         "example"
311/// #     };
312/// # }
313/// #
314/// const HELP: &str = concatdoc! {"
315///     Usage: ", env!("CARGO_BIN_NAME"), " [options]
316///
317///     Options:
318///         -h, --help
319/// "};
320///
321/// print!("{}", HELP);
322/// ```
323///
324/// ```text
325/// Usage: example [options]
326///
327/// Options:
328///     -h, --help
329/// ```
330#[proc_macro]
331pub fn concatdoc(input: TokenStream) -> TokenStream {
332    expand(input, Macro::Concat)
333}
334
335fn expand(input: TokenStream, mode: Macro) -> TokenStream {
336    match try_expand(input, mode) {
337        Ok(tokens) => tokens,
338        Err(err) => err.to_compile_error(),
339    }
340}
341
342fn try_expand(input: TokenStream, mode: Macro) -> Result<TokenStream> {
343    let mut input = input.into_iter().peekable();
344
345    let prefix = match mode {
346        Macro::Indoc | Macro::Format | Macro::Print | Macro::Eprint => None,
347        Macro::Write => {
348            let require_comma = true;
349            let mut expr = expr::parse(&mut input, require_comma)?;
350            expr.extend(iter::once(input.next().unwrap())); // add comma
351            Some(expr)
352        }
353        Macro::Concat => return do_concat(input),
354    };
355
356    let first = input.next().ok_or_else(|| {
357        Error::new(
358            Span::call_site(),
359            "unexpected end of macro invocation, expected format string",
360        )
361    })?;
362
363    let preserve_empty_first_line = false;
364    let unindented_lit = lit_indoc(first, mode, preserve_empty_first_line)?;
365
366    let macro_name = match mode {
367        Macro::Indoc => {
368            require_empty_or_trailing_comma(&mut input)?;
369            return Ok(TokenStream::from(TokenTree::Literal(unindented_lit)));
370        }
371        Macro::Format => "format",
372        Macro::Print => "print",
373        Macro::Eprint => "eprint",
374        Macro::Write => "write",
375        Macro::Concat => ::core::panicking::panic("internal error: entered unreachable code")unreachable!(),
376    };
377
378    // #macro_name! { #unindented_lit #args }
379    Ok(TokenStream::from_iter(<[_]>::into_vec(::alloc::boxed::box_new([TokenTree::Ident(Ident::new(macro_name,
                        Span::call_site())),
                TokenTree::Punct(Punct::new('!', Spacing::Alone)),
                TokenTree::Group(Group::new(Delimiter::Brace,
                        prefix.unwrap_or_else(TokenStream::new).into_iter().chain(iter::once(TokenTree::Literal(unindented_lit))).chain(input).collect()))]))vec![
380        TokenTree::Ident(Ident::new(macro_name, Span::call_site())),
381        TokenTree::Punct(Punct::new('!', Spacing::Alone)),
382        TokenTree::Group(Group::new(
383            Delimiter::Brace,
384            prefix
385                .unwrap_or_else(TokenStream::new)
386                .into_iter()
387                .chain(iter::once(TokenTree::Literal(unindented_lit)))
388                .chain(input)
389                .collect(),
390        )),
391    ]))
392}
393
394fn do_concat(mut input: Peekable<TokenIter>) -> Result<TokenStream> {
395    let mut result = TokenStream::new();
396    let mut first = true;
397
398    while input.peek().is_some() {
399        let require_comma = false;
400        let mut expr = expr::parse(&mut input, require_comma)?;
401        let mut expr_tokens = expr.clone().into_iter();
402        if let Some(token) = expr_tokens.next() {
403            if expr_tokens.next().is_none() {
404                let preserve_empty_first_line = !first;
405                if let Ok(literal) = lit_indoc(token, Macro::Concat, preserve_empty_first_line) {
406                    result.extend(iter::once(TokenTree::Literal(literal)));
407                    expr = TokenStream::new();
408                }
409            }
410        }
411        result.extend(expr);
412        if let Some(comma) = input.next() {
413            result.extend(iter::once(comma));
414        } else {
415            break;
416        }
417        first = false;
418    }
419
420    // concat! { #result }
421    Ok(TokenStream::from_iter(<[_]>::into_vec(::alloc::boxed::box_new([TokenTree::Ident(Ident::new("concat",
                        Span::call_site())),
                TokenTree::Punct(Punct::new('!', Spacing::Alone)),
                TokenTree::Group(Group::new(Delimiter::Brace, result))]))vec![
422        TokenTree::Ident(Ident::new("concat", Span::call_site())),
423        TokenTree::Punct(Punct::new('!', Spacing::Alone)),
424        TokenTree::Group(Group::new(Delimiter::Brace, result)),
425    ]))
426}
427
428fn lit_indoc(token: TokenTree, mode: Macro, preserve_empty_first_line: bool) -> Result<Literal> {
429    let span = token.span();
430    let mut single_token = Some(token);
431
432    while let Some(TokenTree::Group(group)) = single_token {
433        single_token = if group.delimiter() == Delimiter::None {
434            let mut token_iter = group.stream().into_iter();
435            token_iter.next().xor(token_iter.next())
436        } else {
437            None
438        };
439    }
440
441    let single_token =
442        single_token.ok_or_else(|| Error::new(span, "argument must be a single string literal"))?;
443
444    let repr = single_token.to_string();
445    let is_string = repr.starts_with('"') || repr.starts_with('r');
446    let is_byte_string = repr.starts_with("b\"") || repr.starts_with("br");
447    let is_c_string = repr.starts_with("c\"") || repr.starts_with("cr");
448
449    if !is_string && !is_byte_string && !is_c_string {
450        return Err(Error::new(span, "argument must be a single string literal"));
451    }
452
453    if let Some(restricted_kind) = if is_byte_string {
454        Some("byte strings")
455    } else if is_c_string {
456        Some("C-strings")
457    } else {
458        None
459    } {
460        match mode {
461            Macro::Indoc => {}
462            Macro::Format | Macro::Print | Macro::Eprint | Macro::Write => {
463                return Err(Error::new(
464                    span,
465                    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} are not supported in formatting macros",
                restricted_kind))
    })format!("{restricted_kind} are not supported in formatting macros"),
466                ));
467            }
468            Macro::Concat => {
469                return Err(Error::new(
470                    span,
471                    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} are not supported in concat macro",
                restricted_kind))
    })format!("{restricted_kind} are not supported in concat macro"),
472                ));
473            }
474        }
475    }
476
477    let begin = repr.find('"').unwrap() + 1;
478    let end = repr.rfind('"').unwrap();
479    let repr = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}{1}{2}", &repr[..begin],
                do_unindent(&repr[begin..end], preserve_empty_first_line),
                &repr[end..]))
    })format!(
480        "{open}{content}{close}",
481        open = &repr[..begin],
482        content = do_unindent(&repr[begin..end], preserve_empty_first_line),
483        close = &repr[end..],
484    );
485
486    let mut lit = Literal::from_str(&repr).unwrap();
487    lit.set_span(span);
488    Ok(lit)
489}
490
491fn require_empty_or_trailing_comma(input: &mut Peekable<TokenIter>) -> Result<()> {
492    let first = match input.next() {
493        Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => match input.next() {
494            Some(second) => second,
495            None => return Ok(()),
496        },
497        Some(first) => first,
498        None => return Ok(()),
499    };
500    let last = input.last();
501
502    let begin_span = first.span();
503    let end_span = last.as_ref().map_or(begin_span, TokenTree::span);
504    let msg = ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unexpected {0} in macro invocation; indoc argument must be a single string literal",
                if last.is_some() { "tokens" } else { "token" }))
    })format!(
505        "unexpected {token} in macro invocation; indoc argument must be a single string literal",
506        token = if last.is_some() { "tokens" } else { "token" }
507    );
508    Err(Error::new2(begin_span, end_span, &msg))
509}