1#![doc(html_root_url = "https://docs.rs/indoc/2.0.7")]
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;
137#[allow(dead_code)]
138mod unindent;
139
140use crate::error::{Error, Result};
141use crate::unindent::do_unindent;
142use proc_macro::token_stream::IntoIter as TokenIter;
143use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
144use std::iter::{self, Peekable};
145use std::str::FromStr;
146
147#[derive(#[automatically_derived]
impl ::core::marker::Copy for Macro { }Copy, #[automatically_derived]
impl ::core::clone::Clone for Macro {
#[inline]
fn clone(&self) -> Macro { *self }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for Macro {
#[inline]
fn eq(&self, other: &Macro) -> bool {
let __self_discr = ::core::intrinsics::discriminant_value(self);
let __arg1_discr = ::core::intrinsics::discriminant_value(other);
__self_discr == __arg1_discr
}
}PartialEq)]
148enum Macro {
149 Indoc,
150 Format,
151 Print,
152 Eprint,
153 Write,
154 Concat,
155}
156
157#[proc_macro]
183pub fn indoc(input: TokenStream) -> TokenStream {
184 expand(input, Macro::Indoc)
185}
186
187#[proc_macro]
211pub fn formatdoc(input: TokenStream) -> TokenStream {
212 expand(input, Macro::Format)
213}
214
215#[proc_macro]
238pub fn printdoc(input: TokenStream) -> TokenStream {
239 expand(input, Macro::Print)
240}
241
242#[proc_macro]
265pub fn eprintdoc(input: TokenStream) -> TokenStream {
266 expand(input, Macro::Eprint)
267}
268
269#[proc_macro]
295pub fn writedoc(input: TokenStream) -> TokenStream {
296 expand(input, Macro::Write)
297}
298
299#[proc_macro]
331pub fn concatdoc(input: TokenStream) -> TokenStream {
332 expand(input, Macro::Concat)
333}
334
335fn expand(input: TokenStream, mode: Macro) -> TokenStream {
336 match try_expand(input, mode) {
337 Ok(tokens) => tokens,
338 Err(err) => err.to_compile_error(),
339 }
340}
341
342fn try_expand(input: TokenStream, mode: Macro) -> Result<TokenStream> {
343 let mut input = input.into_iter().peekable();
344
345 let prefix = match mode {
346 Macro::Indoc | Macro::Format | Macro::Print | Macro::Eprint => None,
347 Macro::Write => {
348 let require_comma = true;
349 let mut expr = expr::parse(&mut input, require_comma)?;
350 expr.extend(iter::once(input.next().unwrap())); Some(expr)
352 }
353 Macro::Concat => return do_concat(input),
354 };
355
356 let first = input.next().ok_or_else(|| {
357 Error::new(
358 Span::call_site(),
359 "unexpected end of macro invocation, expected format string",
360 )
361 })?;
362
363 let preserve_empty_first_line = false;
364 let unindented_lit = lit_indoc(first, mode, preserve_empty_first_line)?;
365
366 let macro_name = match mode {
367 Macro::Indoc => {
368 require_empty_or_trailing_comma(&mut input)?;
369 return Ok(TokenStream::from(TokenTree::Literal(unindented_lit)));
370 }
371 Macro::Format => "format",
372 Macro::Print => "print",
373 Macro::Eprint => "eprint",
374 Macro::Write => "write",
375 Macro::Concat => ::core::panicking::panic("internal error: entered unreachable code")unreachable!(),
376 };
377
378 Ok(TokenStream::from_iter(<[_]>::into_vec(::alloc::boxed::box_new([TokenTree::Ident(Ident::new(macro_name,
Span::call_site())),
TokenTree::Punct(Punct::new('!', Spacing::Alone)),
TokenTree::Group(Group::new(Delimiter::Brace,
prefix.unwrap_or_else(TokenStream::new).into_iter().chain(iter::once(TokenTree::Literal(unindented_lit))).chain(input).collect()))]))vec![
380 TokenTree::Ident(Ident::new(macro_name, Span::call_site())),
381 TokenTree::Punct(Punct::new('!', Spacing::Alone)),
382 TokenTree::Group(Group::new(
383 Delimiter::Brace,
384 prefix
385 .unwrap_or_else(TokenStream::new)
386 .into_iter()
387 .chain(iter::once(TokenTree::Literal(unindented_lit)))
388 .chain(input)
389 .collect(),
390 )),
391 ]))
392}
393
394fn do_concat(mut input: Peekable<TokenIter>) -> Result<TokenStream> {
395 let mut result = TokenStream::new();
396 let mut first = true;
397
398 while input.peek().is_some() {
399 let require_comma = false;
400 let mut expr = expr::parse(&mut input, require_comma)?;
401 let mut expr_tokens = expr.clone().into_iter();
402 if let Some(token) = expr_tokens.next() {
403 if expr_tokens.next().is_none() {
404 let preserve_empty_first_line = !first;
405 if let Ok(literal) = lit_indoc(token, Macro::Concat, preserve_empty_first_line) {
406 result.extend(iter::once(TokenTree::Literal(literal)));
407 expr = TokenStream::new();
408 }
409 }
410 }
411 result.extend(expr);
412 if let Some(comma) = input.next() {
413 result.extend(iter::once(comma));
414 } else {
415 break;
416 }
417 first = false;
418 }
419
420 Ok(TokenStream::from_iter(<[_]>::into_vec(::alloc::boxed::box_new([TokenTree::Ident(Ident::new("concat",
Span::call_site())),
TokenTree::Punct(Punct::new('!', Spacing::Alone)),
TokenTree::Group(Group::new(Delimiter::Brace, result))]))vec![
422 TokenTree::Ident(Ident::new("concat", Span::call_site())),
423 TokenTree::Punct(Punct::new('!', Spacing::Alone)),
424 TokenTree::Group(Group::new(Delimiter::Brace, result)),
425 ]))
426}
427
428fn lit_indoc(token: TokenTree, mode: Macro, preserve_empty_first_line: bool) -> Result<Literal> {
429 let span = token.span();
430 let mut single_token = Some(token);
431
432 while let Some(TokenTree::Group(group)) = single_token {
433 single_token = if group.delimiter() == Delimiter::None {
434 let mut token_iter = group.stream().into_iter();
435 token_iter.next().xor(token_iter.next())
436 } else {
437 None
438 };
439 }
440
441 let single_token =
442 single_token.ok_or_else(|| Error::new(span, "argument must be a single string literal"))?;
443
444 let repr = single_token.to_string();
445 let is_string = repr.starts_with('"') || repr.starts_with('r');
446 let is_byte_string = repr.starts_with("b\"") || repr.starts_with("br");
447 let is_c_string = repr.starts_with("c\"") || repr.starts_with("cr");
448
449 if !is_string && !is_byte_string && !is_c_string {
450 return Err(Error::new(span, "argument must be a single string literal"));
451 }
452
453 if let Some(restricted_kind) = if is_byte_string {
454 Some("byte strings")
455 } else if is_c_string {
456 Some("C-strings")
457 } else {
458 None
459 } {
460 match mode {
461 Macro::Indoc => {}
462 Macro::Format | Macro::Print | Macro::Eprint | Macro::Write => {
463 return Err(Error::new(
464 span,
465 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} are not supported in formatting macros",
restricted_kind))
})format!("{restricted_kind} are not supported in formatting macros"),
466 ));
467 }
468 Macro::Concat => {
469 return Err(Error::new(
470 span,
471 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} are not supported in concat macro",
restricted_kind))
})format!("{restricted_kind} are not supported in concat macro"),
472 ));
473 }
474 }
475 }
476
477 let begin = repr.find('"').unwrap() + 1;
478 let end = repr.rfind('"').unwrap();
479 let repr = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}{1}{2}", &repr[..begin],
do_unindent(&repr[begin..end], preserve_empty_first_line),
&repr[end..]))
})format!(
480 "{open}{content}{close}",
481 open = &repr[..begin],
482 content = do_unindent(&repr[begin..end], preserve_empty_first_line),
483 close = &repr[end..],
484 );
485
486 let mut lit = Literal::from_str(&repr).unwrap();
487 lit.set_span(span);
488 Ok(lit)
489}
490
491fn require_empty_or_trailing_comma(input: &mut Peekable<TokenIter>) -> Result<()> {
492 let first = match input.next() {
493 Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => match input.next() {
494 Some(second) => second,
495 None => return Ok(()),
496 },
497 Some(first) => first,
498 None => return Ok(()),
499 };
500 let last = input.last();
501
502 let begin_span = first.span();
503 let end_span = last.as_ref().map_or(begin_span, TokenTree::span);
504 let msg = ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("unexpected {0} in macro invocation; indoc argument must be a single string literal",
if last.is_some() { "tokens" } else { "token" }))
})format!(
505 "unexpected {token} in macro invocation; indoc argument must be a single string literal",
506 token = if last.is_some() { "tokens" } else { "token" }
507 );
508 Err(Error::new2(begin_span, end_span, &msg))
509}