510 lines
13 KiB
Rust
510 lines
13 KiB
Rust
//! [![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
|
|
//!
|
|
//! <br>
|
|
//!
|
|
//! 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"
|
|
//! ```
|
|
//!
|
|
//! <br>
|
|
//!
|
|
//! # 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[..]);
|
|
//! }
|
|
//! ```
|
|
//!
|
|
//! <br><br>
|
|
//!
|
|
//! # 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",
|
|
//! }
|
|
//! }
|
|
//! ```
|
|
//!
|
|
//! <br><br>
|
|
//!
|
|
//! # 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<TokenStream> {
|
|
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<TokenIter>) -> Result<TokenStream> {
|
|
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<Literal> {
|
|
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<TokenIter>) -> 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))
|
|
}
|