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.5")]
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;
137mod unindent;
138
139use crate::error::{Error, Result};
140use crate::unindent::do_unindent;
141use proc_macro::token_stream::IntoIter as TokenIter;
142use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
143use std::iter::{self, Peekable};
144use std::str::FromStr;
145
146#[derive(Copy, Clone, PartialEq)]
147enum Macro {
148    Indoc,
149    Format,
150    Print,
151    Eprint,
152    Write,
153    Concat,
154}
155
156/// Unindent and produce `&'static str` or `&'static [u8]`.
157///
158/// Supports normal strings, raw strings, bytestrings, and raw bytestrings.
159///
160/// # Example
161///
162/// ```
163/// # use indoc::indoc;
164/// #
165/// // The type of `program` is &'static str
166/// let program = indoc! {"
167///     def hello():
168///         print('Hello, world!')
169///
170///     hello()
171/// "};
172/// print!("{}", program);
173/// ```
174///
175/// ```text
176/// def hello():
177///     print('Hello, world!')
178///
179/// hello()
180/// ```
181#[proc_macro]
182pub fn indoc(input: TokenStream) -> TokenStream {
183    expand(input, Macro::Indoc)
184}
185
186/// Unindent and call `format!`.
187///
188/// Argument syntax is the same as for [`std::format!`].
189///
190/// # Example
191///
192/// ```
193/// # use indoc::formatdoc;
194/// #
195/// let request = formatdoc! {"
196///     GET {url}
197///     Accept: {mime}
198///     ",
199///     url = "http://localhost:8080",
200///     mime = "application/json",
201/// };
202/// println!("{}", request);
203/// ```
204///
205/// ```text
206/// GET http://localhost:8080
207/// Accept: application/json
208/// ```
209#[proc_macro]
210pub fn formatdoc(input: TokenStream) -> TokenStream {
211    expand(input, Macro::Format)
212}
213
214/// Unindent and call `print!`.
215///
216/// Argument syntax is the same as for [`std::print!`].
217///
218/// # Example
219///
220/// ```
221/// # use indoc::printdoc;
222/// #
223/// printdoc! {"
224///     GET {url}
225///     Accept: {mime}
226///     ",
227///     url = "http://localhost:8080",
228///     mime = "application/json",
229/// }
230/// ```
231///
232/// ```text
233/// GET http://localhost:8080
234/// Accept: application/json
235/// ```
236#[proc_macro]
237pub fn printdoc(input: TokenStream) -> TokenStream {
238    expand(input, Macro::Print)
239}
240
241/// Unindent and call `eprint!`.
242///
243/// Argument syntax is the same as for [`std::eprint!`].
244///
245/// # Example
246///
247/// ```
248/// # use indoc::eprintdoc;
249/// #
250/// eprintdoc! {"
251///     GET {url}
252///     Accept: {mime}
253///     ",
254///     url = "http://localhost:8080",
255///     mime = "application/json",
256/// }
257/// ```
258///
259/// ```text
260/// GET http://localhost:8080
261/// Accept: application/json
262/// ```
263#[proc_macro]
264pub fn eprintdoc(input: TokenStream) -> TokenStream {
265    expand(input, Macro::Eprint)
266}
267
268/// Unindent and call `write!`.
269///
270/// Argument syntax is the same as for [`std::write!`].
271///
272/// # Example
273///
274/// ```
275/// # use indoc::writedoc;
276/// # use std::io::Write;
277/// #
278/// let _ = writedoc!(
279///     std::io::stdout(),
280///     "
281///         GET {url}
282///         Accept: {mime}
283///     ",
284///     url = "http://localhost:8080",
285///     mime = "application/json",
286/// );
287/// ```
288///
289/// ```text
290/// GET http://localhost:8080
291/// Accept: application/json
292/// ```
293#[proc_macro]
294pub fn writedoc(input: TokenStream) -> TokenStream {
295    expand(input, Macro::Write)
296}
297
298/// Unindent and call `concat!`.
299///
300/// Argument syntax is the same as for [`std::concat!`].
301///
302/// # Example
303///
304/// ```
305/// # use indoc::concatdoc;
306/// #
307/// # macro_rules! env {
308/// #     ($var:literal) => {
309/// #         "example"
310/// #     };
311/// # }
312/// #
313/// const HELP: &str = concatdoc! {"
314///     Usage: ", env!("CARGO_BIN_NAME"), " [options]
315///
316///     Options:
317///         -h, --help
318/// "};
319///
320/// print!("{}", HELP);
321/// ```
322///
323/// ```text
324/// Usage: example [options]
325///
326/// Options:
327///     -h, --help
328/// ```
329#[proc_macro]
330pub fn concatdoc(input: TokenStream) -> TokenStream {
331    expand(input, Macro::Concat)
332}
333
334fn expand(input: TokenStream, mode: Macro) -> TokenStream {
335    match try_expand(input, mode) {
336        Ok(tokens) => tokens,
337        Err(err) => err.to_compile_error(),
338    }
339}
340
341fn try_expand(input: TokenStream, mode: Macro) -> Result<TokenStream> {
342    let mut input = input.into_iter().peekable();
343
344    let prefix = match mode {
345        Macro::Indoc | Macro::Format | Macro::Print | Macro::Eprint => None,
346        Macro::Write => {
347            let require_comma = true;
348            let mut expr = expr::parse(&mut input, require_comma)?;
349            expr.extend(iter::once(input.next().unwrap())); // add comma
350            Some(expr)
351        }
352        Macro::Concat => return do_concat(input),
353    };
354
355    let first = input.next().ok_or_else(|| {
356        Error::new(
357            Span::call_site(),
358            "unexpected end of macro invocation, expected format string",
359        )
360    })?;
361
362    let preserve_empty_first_line = false;
363    let unindented_lit = lit_indoc(first, mode, preserve_empty_first_line)?;
364
365    let macro_name = match mode {
366        Macro::Indoc => {
367            require_empty_or_trailing_comma(&mut input)?;
368            return Ok(TokenStream::from(TokenTree::Literal(unindented_lit)));
369        }
370        Macro::Format => "format",
371        Macro::Print => "print",
372        Macro::Eprint => "eprint",
373        Macro::Write => "write",
374        Macro::Concat => unreachable!(),
375    };
376
377    // #macro_name! { #unindented_lit #args }
378    Ok(TokenStream::from_iter(vec![
379        TokenTree::Ident(Ident::new(macro_name, Span::call_site())),
380        TokenTree::Punct(Punct::new('!', Spacing::Alone)),
381        TokenTree::Group(Group::new(
382            Delimiter::Brace,
383            prefix
384                .unwrap_or_else(TokenStream::new)
385                .into_iter()
386                .chain(iter::once(TokenTree::Literal(unindented_lit)))
387                .chain(input)
388                .collect(),
389        )),
390    ]))
391}
392
393fn do_concat(mut input: Peekable<TokenIter>) -> Result<TokenStream> {
394    let mut result = TokenStream::new();
395    let mut first = true;
396
397    while input.peek().is_some() {
398        let require_comma = false;
399        let mut expr = expr::parse(&mut input, require_comma)?;
400        let mut expr_tokens = expr.clone().into_iter();
401        if let Some(token) = expr_tokens.next() {
402            if expr_tokens.next().is_none() {
403                let preserve_empty_first_line = !first;
404                if let Ok(literal) = lit_indoc(token, Macro::Concat, preserve_empty_first_line) {
405                    result.extend(iter::once(TokenTree::Literal(literal)));
406                    expr = TokenStream::new();
407                }
408            }
409        }
410        result.extend(expr);
411        if let Some(comma) = input.next() {
412            result.extend(iter::once(comma));
413        } else {
414            break;
415        }
416        first = false;
417    }
418
419    // concat! { #result }
420    Ok(TokenStream::from_iter(vec![
421        TokenTree::Ident(Ident::new("concat", Span::call_site())),
422        TokenTree::Punct(Punct::new('!', Spacing::Alone)),
423        TokenTree::Group(Group::new(Delimiter::Brace, result)),
424    ]))
425}
426
427fn lit_indoc(token: TokenTree, mode: Macro, preserve_empty_first_line: bool) -> Result<Literal> {
428    let span = token.span();
429    let mut single_token = Some(token);
430
431    while let Some(TokenTree::Group(group)) = single_token {
432        single_token = if group.delimiter() == Delimiter::None {
433            let mut token_iter = group.stream().into_iter();
434            token_iter.next().xor(token_iter.next())
435        } else {
436            None
437        };
438    }
439
440    let single_token =
441        single_token.ok_or_else(|| Error::new(span, "argument must be a single string literal"))?;
442
443    let repr = single_token.to_string();
444    let is_string = repr.starts_with('"') || repr.starts_with('r');
445    let is_byte_string = repr.starts_with("b\"") || repr.starts_with("br");
446
447    if !is_string && !is_byte_string {
448        return Err(Error::new(span, "argument must be a single string literal"));
449    }
450
451    if is_byte_string {
452        match mode {
453            Macro::Indoc => {}
454            Macro::Format | Macro::Print | Macro::Eprint | Macro::Write => {
455                return Err(Error::new(
456                    span,
457                    "byte strings are not supported in formatting macros",
458                ));
459            }
460            Macro::Concat => {
461                return Err(Error::new(
462                    span,
463                    "byte strings are not supported in concat macro",
464                ));
465            }
466        }
467    }
468
469    let begin = repr.find('"').unwrap() + 1;
470    let end = repr.rfind('"').unwrap();
471    let repr = format!(
472        "{open}{content}{close}",
473        open = &repr[..begin],
474        content = do_unindent(&repr[begin..end], preserve_empty_first_line),
475        close = &repr[end..],
476    );
477
478    let mut lit = Literal::from_str(&repr).unwrap();
479    lit.set_span(span);
480    Ok(lit)
481}
482
483fn require_empty_or_trailing_comma(input: &mut Peekable<TokenIter>) -> Result<()> {
484    let first = match input.next() {
485        Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => match input.next() {
486            Some(second) => second,
487            None => return Ok(()),
488        },
489        Some(first) => first,
490        None => return Ok(()),
491    };
492    let last = input.last();
493
494    let begin_span = first.span();
495    let end_span = last.as_ref().map_or(begin_span, TokenTree::span);
496    let msg = format!(
497        "unexpected {token} in macro invocation; indoc argument must be a single string literal",
498        token = if last.is_some() { "tokens" } else { "token" }
499    );
500    Err(Error::new2(begin_span, end_span, &msg))
501}