Format Specifiers
CodeBlock format strings use %-prefixed specifiers to interpolate arguments. Each specifier consumes one argument from the args list (except %W, %>, %<, %[, %], and %%, which consume none).
Quick Reference
| Specifier | Name | Argument | Purpose |
|---|---|---|---|
%T | Type | TypeName | Emit type reference, track import |
%N | Name | NameArg | Emit identifier name |
%S | String | StringLitArg | Emit escaped string literal |
%V | Verbatim | VerbatimStrArg | Emit string with interpolation preserved |
%R | Remark | CommentArg | Emit inline comment |
%L | Literal | &str, String, CodeBlock, CodeFragment | Emit raw value or nested block/fragment |
%W | Wrap | (none) | Soft line break point |
%> | Indent | (none) | Increase indent level |
%< | Dedent | (none) | Decrease indent level |
%[ | Begin | (none) | Start of statement |
%] | End | (none) | End of statement |
%% | Escape | (none) | Literal % character |
%T – Type Reference
The most powerful specifier. Takes a TypeName and does two things: emits the type name in the output AND registers the import so FileSpec::render() can collect, deduplicate, and emit import headers automatically.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::type_name::TypeName;
fn main() {
let user = TypeName::importable("./models", "User");
let block = CodeBlock::of("const u: %T = getUser()", (user,)).unwrap();
// Value import (not `import type`):
// import { User } from './models';
// const u: User = getUser();
}
For type-only imports (TypeScript’s import type), use importable_type:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let user = TypeName::importable_type("./models", "User");
// import type { User } from './models';
}
Generic types track imports recursively. Every TypeName nested inside the generic’s parameters is collected:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let promise = TypeName::generic(
TypeName::primitive("Promise"),
vec![TypeName::importable("./models", "User")],
);
let block = CodeBlock::of("function load(): %T", (promise,)).unwrap();
// Promise<User> -- the User import is still tracked
}
%N – Name
Emits an identifier with automatic keyword escaping. If the name collides with a reserved word in the target language, it is escaped using the language’s convention (Rust: r#type, Go/Python: type_). Bare &str and String values map to Arg::Literal (for %L) by default, so you must use the NameArg wrapper when your format string contains %N.
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, NameArg};
use sigil_stitch::prelude::*;
fn main() {
let method_name = "getData";
let mut cb = CodeBlock::builder();
cb.add_statement("this.%N()", (NameArg(method_name.to_string()),));
let block = cb.build().unwrap();
// Output: this.getData();
}
Reserved-word escaping happens at render time based on the target language:
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, NameArg};
use sigil_stitch::lang::rust::Rust;
use sigil_stitch::spec::file_spec::FileSpec;
use sigil_stitch::prelude::*;
fn main() {
let field_name = "type"; // reserved in Rust
let block = CodeBlock::of("let %N = value", NameArg(field_name.into())).unwrap();
let file = FileSpec::builder_with("test.rs", Rust::new())
.add_code(block)
.build()
.unwrap();
let output = file.render(80).unwrap();
// Output: let r#type = value
}
%S – String Literal
Emits a language-aware quoted string. The CodeLang::render_string_literal() method on each language controls the quoting style and escape rules. TypeScript and JavaScript default to single quotes; Rust, Java, Go, C, C++, Swift, and Kotlin use double quotes; Dart uses single quotes; Python uses single quotes.
Requires the StringLitArg wrapper.
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, StringLitArg};
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
cb.add_statement("const msg = %S", (StringLitArg("hello world".to_string()),));
let block = cb.build().unwrap();
// TypeScript output: const msg = 'hello world';
// Java output: const msg = "hello world";
}
Special characters are escaped according to each language’s rules. For example, Kotlin and Dart escape $ to prevent string interpolation.
%V – Verbatim String Literal
Emits a string with minimal escaping — only characters that would structurally break the string delimiter are escaped, while interpolation sigils ($, `, {, etc.) are preserved as-is. This is useful for generating code that uses the target language’s string interpolation.
Requires the VerbatimStrArg wrapper.
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, VerbatimStrArg};
use sigil_stitch::lang::bash::Bash;
use sigil_stitch::spec::file_spec::FileSpec;
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
cb.add("local config=%V", (VerbatimStrArg("\"${XDG_CONFIG_HOME:-$HOME/.config}\"".to_string()),));
cb.add_line();
cb.add("local version=%V", (VerbatimStrArg("\"$(git describe --tags 2>/dev/null || echo dev)\"".to_string()),));
cb.add_line();
cb.add("echo %V", (VerbatimStrArg("Deploying ${APP_NAME} v${version} (PID=$$)".to_string()),));
let block = cb.build().unwrap();
let file = FileSpec::builder_with("test.bash", Bash::new())
.add_code(block)
.build()
.unwrap();
let output = file.render(80).unwrap();
assert!(output.contains(r#""${XDG_CONFIG_HOME:-$HOME/.config}""#));
assert!(output.contains(r#""$(git describe --tags 2>/dev/null || echo dev)""#));
assert!(output.contains("Deploying ${APP_NAME} v${version} (PID=$$)"));
// Output (Bash $V is pure passthrough — users include their own quotes):
// local config="${XDG_CONFIG_HOME:-$HOME/.config}"
// local version="$(git describe --tags 2>/dev/null || echo dev)"
// echo Deploying ${APP_NAME} v${version} (PID=$$)
}
Per-language behavior:
| Language | %V output for "$x" | Delimiter | Escapes only |
|---|---|---|---|
| Bash/Zsh | $x | (passthrough) | (none) |
| JavaScript/TS | `$x` | `...` | \ ` |
| Python | f"$x" | f"..." | \ " |
| Kotlin/Swift | "$x" | "..." | \ " |
| Dart | '$x' | '...' | \ ' |
| C# | $"$x" | $"..." | \ " |
| Scala | s"$x" | s"..." | \ " |
| Others | Same as %S | (full escaping) | All |
For Bash/Zsh, %V is pure passthrough — the string is emitted as-is with no wrapping quotes and no escaping. Shell interpolates by default, and users control quoting in the %V content itself (include "..." in the string when quoting is desired in the output).
For languages without string interpolation (C, C++, Go, Rust, Java, Haskell, OCaml, Lua), %V falls back to %S behavior (full escaping).
%R – Inline Comment
Emits a language-specific inline comment. Requires the CommentArg wrapper.
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, CommentArg};
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
cb.add_statement("const x = 42; %R", (CommentArg("TODO: validate".to_string()),));
let block = cb.build().unwrap();
// TypeScript: const x = 42; // TODO: validate
// Python: const x = 42; # TODO: validate
}
The comment prefix (//, #, --, etc.) is determined by the target language’s
comment_syntax(). The comment text is emitted verbatim after the prefix with a
single space separator.
In sigil_quote!, inline $comment(expr) after a statement expands to %R:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
sigil_quote!(TypeScript {
doStuff() $comment("cleanup")
}).unwrap();
// Equivalent builder call:
// cb.add("doStuff() %R", (CommentArg("cleanup".to_string()),));
}
@{expr} interpolation in $V and $L
When using $V or $L with a string literal in sigil_quote!, you can embed Rust expressions with @{expr}. These are evaluated at compile time and spliced into the output:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let registry = "ghcr.io";
let tag = "latest";
let block = sigil_quote!(Bash {
docker push $V("@{registry}/myapp:@{tag}")
}).unwrap();
// Output: docker push ghcr.io/myapp:latest
}
Use $V when you want the result wrapped in the target language’s string delimiter (backticks for JS/TS, f"..." for Python, etc.). Use $L when you need plain unwrapped output — type expressions, switch headers, return statements, and other non-string-literal contexts:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let disc = "foo.bar";
let block = sigil_quote!(TypeScript {
switch ($L("@{disc}")) {
$L("case 1:") {
break;
}
}
}).unwrap();
// Output: switch (foo.bar) {
// (No backticks — $L emits plain text, $V would wrap in `...`)
}
This is syntactic sugar — the macro transforms the string into a format!() call. Shell variables like $HOME pass through unchanged while @{expr} parts are resolved at Rust compile time.
Escape @@ to emit a literal @:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let block = sigil_quote!(Bash {
echo $V("admin@@localhost")
}).unwrap();
// Output: echo admin@localhost
}
Arbitrary Rust expressions work inside @{...}, including method calls:
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
let items = vec!["a", "b", "c"];
let block = sigil_quote!(Bash {
echo $V("count=@{items.len()}")
}).unwrap();
// Output: echo count=3
}
If the expression is not a string literal (e.g. $V(my_var) or $L(format!(...))), @{...} processing is skipped and the expression is used as-is.
%L – Literal and Nested Code
Emits raw literal text or structured nested code. Bare &str and String
arguments map to raw Arg::Literal, so no wrapper is needed for ordinary
language text. %L also accepts CodeBlock and CodeFragment for nested code
that should keep imports, indentation, and other structure. Supports @{expr}
interpolation inside string literals in sigil_quote! (see above).
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, CodeFragment};
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
// Bare string -> Arg::Literal -> used by %L
cb.add_statement("const count = %L", "42");
// Nested CodeBlock -> Arg::Code -> also used by %L
let inner = CodeBlock::of("getValue()", ()).unwrap();
cb.add_statement("const x = %L", inner);
// Parsed CodeFragment -> Arg::Code -> structural markers compose
let branch = CodeFragment::of("if (ready) {\n%>return true;%<\n}", ()).unwrap();
cb.add("%L", branch);
let block = cb.build().unwrap();
// const count = 42;
// const x = getValue();
// if (ready) {
// return true;
// }
}
Raw literal strings are intentionally not reparsed as format strings. If a raw
&str / String passed through %L contains %> or %<, build() returns an
UnresolvedIndentMarker error instead of rendering those markers literally. Use
CodeFragment::of(...) for snippets that contain structural markers.
CodeFragment snippets must balance their own %> / %< markers. A fragment
with %> and no matching %< is rejected because it would leak indentation into
whatever code is rendered after it. If you need indentation to span multiple
builder calls, use CodeBlock::builder() and balance the markers before
build().
%W – Soft Line Break
No argument consumed. Marks a point where the Wadler-Lindig pretty printer (via the pretty crate) MAY insert a line break if the line exceeds the target width passed to FileSpec::render(width). If the line fits within the width, %W renders as a space.
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
cb.add_statement("const result = someFunction(arg1,%Warg2,%Warg3,%Warg4)", ());
let block = cb.build().unwrap();
// At width 80 (fits on one line):
// const result = someFunction(arg1, arg2, arg3, arg4);
//
// At width 40 (wraps):
// const result = someFunction(arg1,
// arg2,
// arg3,
// arg4);
}
Without any %W in a CodeBlock, the renderer does direct string concatenation with indent tracking. When %W is present, it builds a pretty::BoxDoc tree for width-aware layout. BoxDoc (not RcDoc) is used so rendered documents are Send + Sync.
%> and %< – Indent / Dedent
No argument consumed. Manually increase (%>) or decrease (%<) the indent level. Rarely needed directly because begin_control_flow(), next_control_flow(), and end_control_flow() manage indentation automatically. Useful when building custom block structures.
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
cb.add("items: [%>\n", ());
cb.add("'first',\n", ());
cb.add("'second',\n", ());
cb.add("%<]", ());
let block = cb.build().unwrap();
// items: [
// 'first',
// 'second',
// ]
}
Indent depth must balance to zero by the time build() is called. An unbalanced depth produces an UnbalancedIndent error.
%[ and %] – Statement Boundaries
No argument consumed. %[ marks the start of a statement. %] marks the end and appends the language’s statement terminator – ; for TypeScript, Rust, Java, C, C++, Dart; nothing for Python, Go, Kotlin, Swift.
You almost never write these directly. add_statement() wraps your format string in %[...%] and appends a newline automatically:
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
// These produce the same output:
cb.add_statement("const x = 1", ());
cb.add("%[const x = 1%]\n", ());
let block = cb.build().unwrap();
// const x = 1;
// const x = 1;
}
%% – Literal Percent
Emits a literal % character in the output. No argument consumed.
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::prelude::*;
fn main() {
let block = CodeBlock::of("progress: 100%%", ()).unwrap();
// progress: 100%
}
Arguments and the IntoArgs Trait
Every method that accepts a format string (add, add_statement, begin_control_flow, next_control_flow, CodeBlock::of, CodeFragment::of) takes args: impl IntoArgs. This trait converts Rust values into Vec<Arg> for the format engine.
The critical rule: bare strings map to Arg::Literal (consumed by %L), not to Arg::Name or Arg::StringLit. To target %N or %S, use the NameArg and StringLitArg wrappers from sigil_stitch::code_block.
Type-to-Arg Mapping
| Rust Type | Maps To | Consumed By |
|---|---|---|
() | empty vec | (no specifiers) |
TypeName | Arg::TypeName | %T |
&str | Arg::Literal | %L |
String | Arg::Literal | %L |
CodeBlock | Arg::Code | %L |
CodeFragment | Arg::Code | %L |
NameArg(String) | Arg::Name | %N |
StringLitArg(String) | Arg::StringLit | %S |
VerbatimStrArg(String) | Arg::VerbatimStr | %V |
CommentArg(String) | Arg::Comment | %R |
Vec<Arg> | passthrough | any |
Single Argument
When a format string has exactly one specifier, pass the value directly (no tuple needed):
extern crate sigil_stitch;
use sigil_stitch::type_name::TypeName;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::prelude::*;
fn main() {
let user = TypeName::importable("./models", "User");
let block = CodeBlock::of("let u: %T", user).unwrap();
}
Multiple Arguments with Tuples
For two or more specifiers, use a tuple. Tuples are supported up to 8 elements. Each element must implement Into<Arg>.
extern crate sigil_stitch;
use sigil_stitch::code_block::{CodeBlock, StringLitArg};
use sigil_stitch::type_name::TypeName;
use sigil_stitch::prelude::*;
fn main() {
let user_type = TypeName::importable("./models", "User");
// Two args: a TypeName and a StringLitArg
let mut cb = CodeBlock::builder();
cb.add_statement("const u: %T = getUser(%S)", (user_type, StringLitArg("admin".into())));
let block = cb.build().unwrap();
// const u: User = getUser('admin');
}
No Arguments
Pass () when the format string has no specifiers:
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::prelude::*;
fn main() {
let mut cb = CodeBlock::builder();
cb.add_statement("return null", ());
let block = cb.build().unwrap();
}
Argument Count Validation
The builder checks that the number of argument-consuming specifiers (%T, %N, %S, %V, %L) matches the number of arguments provided. A mismatch records a FormatArgCount error, surfaced when build() is called. The error carries the expected specifier list and the actual argument kinds so you can see exactly which slot is wrong.
extern crate sigil_stitch;
use sigil_stitch::prelude::*;
fn main() {
// This will fail: format has 2 specifiers but only 1 argument
let mut cb = CodeBlock::builder();
cb.add_statement("const %N: %T = null", "x"); // &str gives one Arg::Literal
let result = cb.build();
// Err(FormatArgCount {
// format: "const %N: %T = null",
// expected_specifiers: vec!["%N", "%T"],
// actual_arg_kinds: vec!["Literal"],
// })
}
An unrecognised specifier character (anything after % that isn’t T, N, S, V, L, W, >, <, [, ], or %) produces Err(SigilStitchError::InvalidFormatSpecifier { format, specifier }) instead.