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