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 = "1.0"
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 four 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//!
83//! ```
84//! use indoc::printdoc;
85//!
86//! fn main() {
87//! printdoc! {"
88//! GET {url}
89//! Accept: {mime}
90//! ",
91//! url = "http://localhost:8080",
92//! mime = "application/json",
93//! }
94//! }
95//! ```
96//!
97//! <br><br>
98//!
99//! # Explanation
100//!
101//! The following rules characterize the behavior of the `indoc!()` macro:
102//!
103//! 1. Count the leading spaces of each line, ignoring the first line and any
104//! lines that are empty or contain spaces only.
105//! 2. Take the minimum.
106//! 3. If the first line is empty i.e. the string begins with a newline, remove
107//! the first line.
108//! 4. Remove the computed number of spaces from the beginning of each line.
109
110#![allow(
111 clippy::derive_partial_eq_without_eq,
112 clippy::module_name_repetitions,
113 clippy::needless_doctest_main,
114 clippy::needless_pass_by_value,
115 clippy::trivially_copy_pass_by_ref,
116 clippy::type_complexity
117)]
118
119mod error;
120mod expr;
121mod unindent;
122
123use crate::error::{Error, Result};
124use crate::expr::Expr;
125use crate::unindent::unindent;
126use proc_macro::token_stream::IntoIter as TokenIter;
127use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
128use std::iter::{self, FromIterator};
129use std::str::FromStr;
130
131#[derive(Copy, Clone, PartialEq)]
132enum Macro {
133 Indoc,
134 Format,
135 Print,
136 Eprint,
137 Write,
138}
139
140/// Unindent and produce `&'static str`.
141///
142/// # Example
143///
144/// ```
145/// # use indoc::indoc;
146/// #
147/// // The type of `program` is &'static str
148/// let program = indoc! {"
149/// def hello():
150/// print('Hello, world!')
151///
152/// hello()
153/// "};
154/// print!("{}", program);
155/// ```
156///
157/// ```text
158/// def hello():
159/// print('Hello, world!')
160///
161/// hello()
162/// ```
163#[proc_macro]
164pub fn indoc(input: TokenStream) -> TokenStream {
165 expand(input, mode:Macro::Indoc)
166}
167
168/// Unindent and call `format!`.
169///
170/// Argument syntax is the same as for [`std::format!`].
171///
172/// # Example
173///
174/// ```
175/// # use indoc::formatdoc;
176/// #
177/// let request = formatdoc! {"
178/// GET {url}
179/// Accept: {mime}
180/// ",
181/// url = "http://localhost:8080",
182/// mime = "application/json",
183/// };
184/// println!("{}", request);
185/// ```
186///
187/// ```text
188/// GET http://localhost:8080
189/// Accept: application/json
190/// ```
191#[proc_macro]
192pub fn formatdoc(input: TokenStream) -> TokenStream {
193 expand(input, mode:Macro::Format)
194}
195
196/// Unindent and call `print!`.
197///
198/// Argument syntax is the same as for [`std::print!`].
199///
200/// # Example
201///
202/// ```
203/// # use indoc::printdoc;
204/// #
205/// printdoc! {"
206/// GET {url}
207/// Accept: {mime}
208/// ",
209/// url = "http://localhost:8080",
210/// mime = "application/json",
211/// }
212/// ```
213///
214/// ```text
215/// GET http://localhost:8080
216/// Accept: application/json
217/// ```
218#[proc_macro]
219pub fn printdoc(input: TokenStream) -> TokenStream {
220 expand(input, mode:Macro::Print)
221}
222
223/// Unindent and call `eprint!`.
224///
225/// Argument syntax is the same as for [`std::eprint!`].
226///
227/// # Example
228///
229/// ```
230/// # use indoc::eprintdoc;
231/// #
232/// eprintdoc! {"
233/// GET {url}
234/// Accept: {mime}
235/// ",
236/// url = "http://localhost:8080",
237/// mime = "application/json",
238/// }
239/// ```
240///
241/// ```text
242/// GET http://localhost:8080
243/// Accept: application/json
244/// ```
245#[proc_macro]
246pub fn eprintdoc(input: TokenStream) -> TokenStream {
247 expand(input, mode:Macro::Eprint)
248}
249
250/// Unindent and call `write!`.
251///
252/// Argument syntax is the same as for [`std::write!`].
253///
254/// # Example
255///
256/// ```
257/// # use indoc::writedoc;
258/// # use std::io::Write;
259/// #
260/// let _ = writedoc!(
261/// std::io::stdout(),
262/// "
263/// GET {url}
264/// Accept: {mime}
265/// ",
266/// url = "http://localhost:8080",
267/// mime = "application/json",
268/// );
269/// ```
270///
271/// ```text
272/// GET http://localhost:8080
273/// Accept: application/json
274/// ```
275#[proc_macro]
276pub fn writedoc(input: TokenStream) -> TokenStream {
277 expand(input, mode:Macro::Write)
278}
279
280fn expand(input: TokenStream, mode: Macro) -> TokenStream {
281 match try_expand(input, mode) {
282 Ok(tokens: TokenStream) => tokens,
283 Err(err: Error) => err.to_compile_error(),
284 }
285}
286
287fn try_expand(input: TokenStream, mode: Macro) -> Result<TokenStream> {
288 let mut input = input.into_iter();
289
290 let prefix = if mode == Macro::Write {
291 Some(expr::parse(&mut input)?)
292 } else {
293 None
294 };
295
296 let first = input.next().ok_or_else(|| {
297 Error::new(
298 Span::call_site(),
299 "unexpected end of macro invocation, expected format string",
300 )
301 })?;
302
303 let unindented_lit = lit_indoc(first, mode)?;
304
305 let macro_name = match mode {
306 Macro::Indoc => {
307 require_empty_or_trailing_comma(&mut input)?;
308 return Ok(TokenStream::from(TokenTree::Literal(unindented_lit)));
309 }
310 Macro::Format => "format",
311 Macro::Print => "print",
312 Macro::Eprint => "eprint",
313 Macro::Write => "write",
314 };
315
316 // #macro_name! { #unindented_lit #args }
317 Ok(TokenStream::from_iter(vec![
318 TokenTree::Ident(Ident::new(macro_name, Span::call_site())),
319 TokenTree::Punct(Punct::new('!', Spacing::Alone)),
320 TokenTree::Group(Group::new(
321 Delimiter::Brace,
322 prefix
323 .map_or_else(TokenStream::new, Expr::into_tokens)
324 .into_iter()
325 .chain(iter::once(TokenTree::Literal(unindented_lit)))
326 .chain(input)
327 .collect(),
328 )),
329 ]))
330}
331
332fn lit_indoc(token: TokenTree, mode: Macro) -> Result<Literal> {
333 let span = token.span();
334 let mut single_token = Some(token);
335
336 while let Some(TokenTree::Group(group)) = single_token {
337 single_token = if group.delimiter() == Delimiter::None {
338 let mut token_iter = group.stream().into_iter();
339 token_iter.next().xor(token_iter.next())
340 } else {
341 None
342 };
343 }
344
345 let single_token =
346 single_token.ok_or_else(|| Error::new(span, "argument must be a single string literal"))?;
347
348 let repr = single_token.to_string();
349 let is_string = repr.starts_with('"') || repr.starts_with('r');
350 let is_byte_string = repr.starts_with("b\"") || repr.starts_with("br");
351
352 if !is_string && !is_byte_string {
353 return Err(Error::new(span, "argument must be a single string literal"));
354 }
355
356 if is_byte_string && mode != Macro::Indoc {
357 return Err(Error::new(
358 span,
359 "byte strings are not supported in formatting macros",
360 ));
361 }
362
363 let begin = repr.find('"').unwrap() + 1;
364 let end = repr.rfind('"').unwrap();
365 let repr = format!(
366 "{open}{content}{close}",
367 open = &repr[..begin],
368 content = unindent(&repr[begin..end]),
369 close = &repr[end..],
370 );
371
372 match TokenStream::from_str(&repr)
373 .unwrap()
374 .into_iter()
375 .next()
376 .unwrap()
377 {
378 TokenTree::Literal(mut lit) => {
379 lit.set_span(span);
380 Ok(lit)
381 }
382 _ => unreachable!(),
383 }
384}
385
386fn require_empty_or_trailing_comma(input: &mut TokenIter) -> Result<()> {
387 let first: TokenTree = match input.next() {
388 Some(TokenTree::Punct(punct: Punct)) if punct.as_char() == ',' => match input.next() {
389 Some(second: TokenTree) => second,
390 None => return Ok(()),
391 },
392 Some(first: TokenTree) => first,
393 None => return Ok(()),
394 };
395 let last: Option = input.last();
396
397 let begin_span: Span = first.span();
398 let end_span: Span = last.as_ref().map_or(default:begin_span, f:TokenTree::span);
399 let msg: String = format!(
400 "unexpected {token} in macro invocation; indoc argument must be a single string literal",
401 token = if last.is_some() { "tokens" } else { "token" }
402 );
403 Err(Error::new2(begin_span, end_span, &msg))
404}
405