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, ...)` — equivalent to `format!(indoc!($fmt), ...)`
79//! - `printdoc!($fmt, ...)` — equivalent to `print!(indoc!($fmt), ...)`
80//! - `eprintdoc!($fmt, ...)` — equivalent to `eprint!(indoc!($fmt), ...)`
81//! - `writedoc!($dest, $fmt, ...)` — equivalent to `write!($dest, indoc!($fmt), ...)`
82//! - `concatdoc!(...)` — 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}