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.6" )] |
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` 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 ] |
182 | pub fn indoc(input: TokenStream) -> TokenStream { |
183 | expand(input, mode: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 ] |
210 | pub fn formatdoc(input: TokenStream) -> TokenStream { |
211 | expand(input, mode: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 ] |
237 | pub fn printdoc(input: TokenStream) -> TokenStream { |
238 | expand(input, mode: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 ] |
264 | pub fn eprintdoc(input: TokenStream) -> TokenStream { |
265 | expand(input, mode: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 ] |
294 | pub fn writedoc(input: TokenStream) -> TokenStream { |
295 | expand(input, mode: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 ] |
330 | pub fn concatdoc(input: TokenStream) -> TokenStream { |
331 | expand(input, mode:Macro::Concat) |
332 | } |
333 | |
334 | fn expand(input: TokenStream, mode: Macro) -> TokenStream { |
335 | match try_expand(input, mode) { |
336 | Ok(tokens: TokenStream) => tokens, |
337 | Err(err: Error) => err.to_compile_error(), |
338 | } |
339 | } |
340 | |
341 | fn 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 | |
393 | fn 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 | |
427 | fn 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 | |
483 | fn require_empty_or_trailing_comma(input: &mut Peekable<TokenIter>) -> Result<()> { |
484 | let first: TokenTree = match input.next() { |
485 | Some(TokenTree::Punct(punct: Punct)) if punct.as_char() == ',' => match input.next() { |
486 | Some(second: TokenTree) => second, |
487 | None => return Ok(()), |
488 | }, |
489 | Some(first: TokenTree) => first, |
490 | None => return Ok(()), |
491 | }; |
492 | let last: Option = input.last(); |
493 | |
494 | let begin_span: Span = first.span(); |
495 | let end_span: Span = last.as_ref().map_or(default:begin_span, f:TokenTree::span); |
496 | let msg: String = 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 | } |
502 | |