indoc

A procedural macro for indented, heredoc-style string literals in Rust, so multiline strings stay readable without breaking your code's indentation.

Library
Cargo
v2.0.7
787stars
MIT OR Apache-2.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
58/100Fair
Development Activity56
Maintenance48
Community40
Maturity60
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
81/100Excellent
Architecture85
Code Quality88
Innovation62
Learning Curve90

indoc gives Rust code an ergonomic way to embed multiline string literals without sacrificing indentation. Normally a multiline string in Rust either has to start in column zero, wrecking the visual flow of the surrounding code, or carries stray leading whitespace on every line that has to be stripped manually at runtime. The indoc! macro solves this at compile time: it takes a string literal written with normal code indentation, computes the minimum common leading whitespace across its lines, and strips exactly that much, so the leftmost non-space character lands in column one.

Beyond the core indoc! macro, the crate ships five companion macros — formatdoc!, printdoc!, eprintdoc!, writedoc!, and concatdoc! — that combine unindenting with the standard library’s format!, print!, eprint!, write!, and concat! respectively, so formatted, printed, or written multiline text never has to be assembled from an already-dedented string first. It works transparently with plain, raw, byte, and C string literals, and the underlying dedent logic is also exposed as a standalone unindent crate for cases where the input string isn’t known at compile time.

What You Get

  • indoc! macro - unindents a multiline string literal at compile time, computing the minimum common leading whitespace and stripping it from every line.
  • Five formatting companion macros - formatdoc!, printdoc!, eprintdoc!, writedoc!, and concatdoc! combine unindenting with format!, print!, eprint!, write!, and concat! in one step.
  • Support for every string literal kind - works with normal, raw, byte, and C string literals with no special syntax required.
  • Companion unindent crate - the same dedent algorithm exposed as a plain function/trait (unindent(), Unindent trait) for strings that aren’t known at compile time.
  • Zero runtime cost - all unindenting happens inside the proc macro at compile time; the emitted code is a plain string literal.

Common Use Cases

  • Embedding readable multiline test fixtures - a test writes an expected multiline output string indented to match the surrounding function body instead of starting at column zero.
  • Building CLI help text and usage strings - a command-line tool defines its --help output as an indented block via formatdoc!/printdoc! rather than a jagged, hard-to-edit literal.
  • Writing SQL, HTTP, or config snippets inline - code that constructs a raw request or query string keeps the snippet legible and indented consistently with the surrounding logic.
  • Generating source code or templates from a proc macro - macro authors compose generated code snippets as readable indented blocks instead of manually escaping and dedenting.

Under The Hood

Architecture indoc is a proc-macro = true crate with a small, tightly scoped internal structure: lib.rs exposes the six public macros (indoc!, formatdoc!, printdoc!, eprintdoc!, writedoc!, concatdoc!) and dispatches each through a single shared try_expand function keyed on a Macro enum, so all six share the same token-parsing and error-reporting path rather than duplicating logic. expr.rs implements a minimal hand-rolled expression parser used only by writedoc!/concatdoc! to split leading destination/format arguments from the trailing string literal, tracking angle-bracket depth so generic types like Vec<T> in an expression aren’t mistaken for comparison operators. error.rs wraps span information into a compile_error! token stream so failures surface as normal Rust compiler diagnostics rather than panics. The actual dedent logic lives in unindent.rs, isolated from the macro-expansion code so it can be reused unmodified by the separate unindent sub-crate. A workspace member (unindent/) re-exports this same file, keeping the two crates’ behavior identical without a shared dependency.

Tech Stack The crate targets Rust edition 2021 with a minimum supported Rust version of 1.71, and depends only on the standard library plus proc_macro — there are no runtime dependencies at all, keeping compile times and supply-chain surface minimal. Dev-dependencies are limited to rustversion (for conditionally compiling in C-string test support on newer toolchains), trybuild (for UI/compile-fail testing), and its own sibling unindent crate. build.rs uses rustversion to emit a cfg flag gating C-string literal tests on Rust ≥1.77, since C-string literal support is newer than the crate’s MSRV. CI runs the full matrix (nightly, beta, stable, and two pinned MSRV-adjacent versions) via GitHub Actions using a shared dtolnay/.github reusable workflow, plus a separate docs.rs build check.

Code Quality Testing is extensive relative to the crate’s size: tests/ contains dedicated files for each public macro (test_indoc.rs, test_formatdoc.rs, test_writedoc.rs, test_concat.rs, test_unindent.rs) plus a tests/ui/ directory of trybuild compile-fail fixtures with matching .stderr snapshots verifying exact diagnostic output for misuse (wrong argument count, non-literal input, byte/C-strings in unsupported macros, etc.). Error handling is explicit throughout — try_expand returns a Result propagated with ?, and failures are converted to well-spanned compile_error! output rather than any panic or silent fallback. The crate enables RUSTFLAGS=-Dwarnings in CI so any compiler or clippy warning fails the build, and several clippy:: lints are deliberately allowed with explicit justification comments rather than silently suppressed. Naming and module boundaries are small and purposeful (error, expr, unindent), consistent with the author’s other widely-used crates.

What Makes It Unique indoc’s core contribution is doing indentation-stripping entirely inside the proc macro at compile time, computing the minimum common leading whitespace across all lines and re-emitting a corrected string literal — so there is no runtime string-processing cost at all, unlike approaches that trim indentation with a helper function called at runtime. Its lookbehind-based angle-bracket tracking in expr.rs is a compact solution to a real parsing edge case (distinguishing </> as generic delimiters versus comparison operators) that a naive token scan would get wrong. Bundling five format-macro variants around one shared literal-unindenting core, while also publishing that core standalone as unindent, lets the same logic serve both compile-time and runtime use cases without duplicating the dedent algorithm.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search