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.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 | |
135 | mod error; |
136 | mod expr; |
137 | mod unindent; |
138 | |
139 | use crate::error::{Error, Result}; |
140 | use crate::unindent::do_unindent; |
141 | use proc_macro::token_stream::IntoIter as TokenIter; |
142 | use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree}; |
143 | use std::iter::{self, Peekable}; |
144 | use std::str::FromStr; |
145 | |
146 | #[derive (Copy, Clone, PartialEq)] |
147 | enum 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 ] |
180 | pub 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 ] |
208 | pub 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 ] |
235 | pub 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 ] |
262 | pub 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 ] |
292 | pub 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 ] |
328 | pub fn concatdoc(input: TokenStream) -> TokenStream { |
329 | expand(input, mode:Macro::Concat) |
330 | } |
331 | |
332 | fn 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 | |
339 | fn 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 | |
391 | fn 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 | |
425 | fn 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 | |
481 | fn 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 | |