//! [![github]](https://github.com/dtolnay/indoc) [![crates-io]](https://crates.io/crates/indoc) [![docs-rs]](https://docs.rs/indoc) //! //! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github //! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust //! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs //! //!
//! //! This crate provides a procedural macro for indented string literals. The //! `indoc!()` macro takes a multiline string literal and un-indents it at //! compile time so the leftmost non-space character is in the first column. //! //! ```toml //! [dependencies] //! indoc = "2" //! ``` //! //!
//! //! # Using indoc //! //! ``` //! use indoc::indoc; //! //! fn main() { //! let testing = indoc! {" //! def hello(): //! print('Hello, world!') //! //! hello() //! "}; //! let expected = "def hello():\n print('Hello, world!')\n\nhello()\n"; //! assert_eq!(testing, expected); //! } //! ``` //! //! Indoc also works with raw string literals: //! //! ``` //! use indoc::indoc; //! //! fn main() { //! let testing = indoc! {r#" //! def hello(): //! print("Hello, world!") //! //! hello() //! "#}; //! let expected = "def hello():\n print(\"Hello, world!\")\n\nhello()\n"; //! assert_eq!(testing, expected); //! } //! ``` //! //! And byte string literals: //! //! ``` //! use indoc::indoc; //! //! fn main() { //! let testing = indoc! {b" //! def hello(): //! print('Hello, world!') //! //! hello() //! "}; //! let expected = b"def hello():\n print('Hello, world!')\n\nhello()\n"; //! assert_eq!(testing[..], expected[..]); //! } //! ``` //! //!

//! //! # Formatting macros //! //! The indoc crate exports five additional macros to substitute conveniently //! for the standard library's formatting macros: //! //! - `formatdoc!($fmt, ...)` — equivalent to `format!(indoc!($fmt), ...)` //! - `printdoc!($fmt, ...)` — equivalent to `print!(indoc!($fmt), ...)` //! - `eprintdoc!($fmt, ...)` — equivalent to `eprint!(indoc!($fmt), ...)` //! - `writedoc!($dest, $fmt, ...)` — equivalent to `write!($dest, indoc!($fmt), ...)` //! - `concatdoc!(...)` — equivalent to `concat!(...)` with each string literal wrapped in `indoc!` //! //! ``` //! # macro_rules! env { //! # ($var:literal) => { //! # "example" //! # }; //! # } //! # //! use indoc::{concatdoc, printdoc}; //! //! const HELP: &str = concatdoc! {" //! Usage: ", env!("CARGO_BIN_NAME"), " [options] //! //! Options: //! -h, --help //! "}; //! //! fn main() { //! printdoc! {" //! GET {url} //! Accept: {mime} //! ", //! url = "http://localhost:8080", //! mime = "application/json", //! } //! } //! ``` //! //!

//! //! # Explanation //! //! The following rules characterize the behavior of the `indoc!()` macro: //! //! 1. Count the leading spaces of each line, ignoring the first line and any //! lines that are empty or contain spaces only. //! 2. Take the minimum. //! 3. If the first line is empty i.e. the string begins with a newline, remove //! the first line. //! 4. Remove the computed number of spaces from the beginning of each line. #![doc(html_root_url = "https://docs.rs/indoc/2.0.7")] #![allow( clippy::derive_partial_eq_without_eq, clippy::from_iter_instead_of_collect, clippy::module_name_repetitions, clippy::needless_doctest_main, clippy::needless_pass_by_value, clippy::trivially_copy_pass_by_ref, clippy::type_complexity )] mod error; mod expr; #[allow(dead_code)] mod unindent; use crate::error::{Error, Result}; use crate::unindent::do_unindent; use proc_macro::token_stream::IntoIter as TokenIter; use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree}; use std::iter::{self, Peekable}; use std::str::FromStr; #[derive(Copy, Clone, PartialEq)] enum Macro { Indoc, Format, Print, Eprint, Write, Concat, } /// Unindent and produce `&'static str` or `&'static [u8]`. /// /// Supports normal strings, raw strings, bytestrings, and raw bytestrings. /// /// # Example /// /// ``` /// # use indoc::indoc; /// # /// // The type of `program` is &'static str /// let program = indoc! {" /// def hello(): /// print('Hello, world!') /// /// hello() /// "}; /// print!("{}", program); /// ``` /// /// ```text /// def hello(): /// print('Hello, world!') /// /// hello() /// ``` #[proc_macro] pub fn indoc(input: TokenStream) -> TokenStream { expand(input, Macro::Indoc) } /// Unindent and call `format!`. /// /// Argument syntax is the same as for [`std::format!`]. /// /// # Example /// /// ``` /// # use indoc::formatdoc; /// # /// let request = formatdoc! {" /// GET {url} /// Accept: {mime} /// ", /// url = "http://localhost:8080", /// mime = "application/json", /// }; /// println!("{}", request); /// ``` /// /// ```text /// GET http://localhost:8080 /// Accept: application/json /// ``` #[proc_macro] pub fn formatdoc(input: TokenStream) -> TokenStream { expand(input, Macro::Format) } /// Unindent and call `print!`. /// /// Argument syntax is the same as for [`std::print!`]. /// /// # Example /// /// ``` /// # use indoc::printdoc; /// # /// printdoc! {" /// GET {url} /// Accept: {mime} /// ", /// url = "http://localhost:8080", /// mime = "application/json", /// } /// ``` /// /// ```text /// GET http://localhost:8080 /// Accept: application/json /// ``` #[proc_macro] pub fn printdoc(input: TokenStream) -> TokenStream { expand(input, Macro::Print) } /// Unindent and call `eprint!`. /// /// Argument syntax is the same as for [`std::eprint!`]. /// /// # Example /// /// ``` /// # use indoc::eprintdoc; /// # /// eprintdoc! {" /// GET {url} /// Accept: {mime} /// ", /// url = "http://localhost:8080", /// mime = "application/json", /// } /// ``` /// /// ```text /// GET http://localhost:8080 /// Accept: application/json /// ``` #[proc_macro] pub fn eprintdoc(input: TokenStream) -> TokenStream { expand(input, Macro::Eprint) } /// Unindent and call `write!`. /// /// Argument syntax is the same as for [`std::write!`]. /// /// # Example /// /// ``` /// # use indoc::writedoc; /// # use std::io::Write; /// # /// let _ = writedoc!( /// std::io::stdout(), /// " /// GET {url} /// Accept: {mime} /// ", /// url = "http://localhost:8080", /// mime = "application/json", /// ); /// ``` /// /// ```text /// GET http://localhost:8080 /// Accept: application/json /// ``` #[proc_macro] pub fn writedoc(input: TokenStream) -> TokenStream { expand(input, Macro::Write) } /// Unindent and call `concat!`. /// /// Argument syntax is the same as for [`std::concat!`]. /// /// # Example /// /// ``` /// # use indoc::concatdoc; /// # /// # macro_rules! env { /// # ($var:literal) => { /// # "example" /// # }; /// # } /// # /// const HELP: &str = concatdoc! {" /// Usage: ", env!("CARGO_BIN_NAME"), " [options] /// /// Options: /// -h, --help /// "}; /// /// print!("{}", HELP); /// ``` /// /// ```text /// Usage: example [options] /// /// Options: /// -h, --help /// ``` #[proc_macro] pub fn concatdoc(input: TokenStream) -> TokenStream { expand(input, Macro::Concat) } fn expand(input: TokenStream, mode: Macro) -> TokenStream { match try_expand(input, mode) { Ok(tokens) => tokens, Err(err) => err.to_compile_error(), } } fn try_expand(input: TokenStream, mode: Macro) -> Result { let mut input = input.into_iter().peekable(); let prefix = match mode { Macro::Indoc | Macro::Format | Macro::Print | Macro::Eprint => None, Macro::Write => { let require_comma = true; let mut expr = expr::parse(&mut input, require_comma)?; expr.extend(iter::once(input.next().unwrap())); // add comma Some(expr) } Macro::Concat => return do_concat(input), }; let first = input.next().ok_or_else(|| { Error::new( Span::call_site(), "unexpected end of macro invocation, expected format string", ) })?; let preserve_empty_first_line = false; let unindented_lit = lit_indoc(first, mode, preserve_empty_first_line)?; let macro_name = match mode { Macro::Indoc => { require_empty_or_trailing_comma(&mut input)?; return Ok(TokenStream::from(TokenTree::Literal(unindented_lit))); } Macro::Format => "format", Macro::Print => "print", Macro::Eprint => "eprint", Macro::Write => "write", Macro::Concat => unreachable!(), }; // #macro_name! { #unindented_lit #args } Ok(TokenStream::from_iter(vec![ 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(), )), ])) } fn do_concat(mut input: Peekable) -> Result { let mut result = TokenStream::new(); let mut first = true; while input.peek().is_some() { let require_comma = false; let mut expr = expr::parse(&mut input, require_comma)?; let mut expr_tokens = expr.clone().into_iter(); if let Some(token) = expr_tokens.next() { if expr_tokens.next().is_none() { let preserve_empty_first_line = !first; if let Ok(literal) = lit_indoc(token, Macro::Concat, preserve_empty_first_line) { result.extend(iter::once(TokenTree::Literal(literal))); expr = TokenStream::new(); } } } result.extend(expr); if let Some(comma) = input.next() { result.extend(iter::once(comma)); } else { break; } first = false; } // concat! { #result } Ok(TokenStream::from_iter(vec![ TokenTree::Ident(Ident::new("concat", Span::call_site())), TokenTree::Punct(Punct::new('!', Spacing::Alone)), TokenTree::Group(Group::new(Delimiter::Brace, result)), ])) } fn lit_indoc(token: TokenTree, mode: Macro, preserve_empty_first_line: bool) -> Result { let span = token.span(); let mut single_token = Some(token); while let Some(TokenTree::Group(group)) = single_token { single_token = if group.delimiter() == Delimiter::None { let mut token_iter = group.stream().into_iter(); token_iter.next().xor(token_iter.next()) } else { None }; } let single_token = single_token.ok_or_else(|| Error::new(span, "argument must be a single string literal"))?; let repr = single_token.to_string(); let is_string = repr.starts_with('"') || repr.starts_with('r'); let is_byte_string = repr.starts_with("b\"") || repr.starts_with("br"); let is_c_string = repr.starts_with("c\"") || repr.starts_with("cr"); if !is_string && !is_byte_string && !is_c_string { return Err(Error::new(span, "argument must be a single string literal")); } if let Some(restricted_kind) = if is_byte_string { Some("byte strings") } else if is_c_string { Some("C-strings") } else { None } { match mode { Macro::Indoc => {} Macro::Format | Macro::Print | Macro::Eprint | Macro::Write => { return Err(Error::new( span, format!("{restricted_kind} are not supported in formatting macros"), )); } Macro::Concat => { return Err(Error::new( span, format!("{restricted_kind} are not supported in concat macro"), )); } } } let begin = repr.find('"').unwrap() + 1; let end = repr.rfind('"').unwrap(); let repr = format!( "{open}{content}{close}", open = &repr[..begin], content = do_unindent(&repr[begin..end], preserve_empty_first_line), close = &repr[end..], ); let mut lit = Literal::from_str(&repr).unwrap(); lit.set_span(span); Ok(lit) } fn require_empty_or_trailing_comma(input: &mut Peekable) -> Result<()> { let first = match input.next() { Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => match input.next() { Some(second) => second, None => return Ok(()), }, Some(first) => first, None => return Ok(()), }; let last = input.last(); let begin_span = first.span(); let end_span = last.as_ref().map_or(begin_span, TokenTree::span); let msg = format!( "unexpected {token} in macro invocation; indoc argument must be a single string literal", token = if last.is_some() { "tokens" } else { "token" } ); Err(Error::new2(begin_span, end_span, &msg)) }