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, ...)` — 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 | //! |
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 | |
119 | mod error; |
120 | mod expr; |
121 | mod unindent; |
122 | |
123 | use crate::error::{Error, Result}; |
124 | use crate::expr::Expr; |
125 | use crate::unindent::unindent; |
126 | use proc_macro::token_stream::IntoIter as TokenIter; |
127 | use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree}; |
128 | use std::iter::{self, FromIterator}; |
129 | use std::str::FromStr; |
130 | |
131 | #[derive (Copy, Clone, PartialEq)] |
132 | enum 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 ] |
164 | pub 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 ] |
192 | pub 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 ] |
219 | pub 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 ] |
246 | pub 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 ] |
276 | pub fn writedoc(input: TokenStream) -> TokenStream { |
277 | expand(input, mode:Macro::Write) |
278 | } |
279 | |
280 | fn 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 | |
287 | fn 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 | |
332 | fn 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 | |
386 | fn 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 | |