- Rust 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
A second %import of the same resolved path re-parsed and re-evaluated the file every time, so two separate top-level documents processed by the same evaluator (as reweave-cli does for every input in one --dir batch) could not both %import the same shared macro fragment: the second one redefined every macro the first one had already defined and failed as a duplicate binding. %import only exists to bring definitions into scope, and once a path's macros/variables are defined they stay visible for the rest of the run, so a repeat import is now a silent no-op (tracked via a new imported_paths set on EvaluatorState). %include is unchanged - it always re-pastes the file's output at each call site, which is its whole point. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
| docs | ||
| examples | ||
| reweave-cli | ||
| reweave-core | ||
| reweave-macro | ||
| reweave-tangle | ||
| scripts | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
Reweave
Reweave is a small Markdown-to-files tool for projects that want literate-style source assembly without a database, reverse mapping, or generated-file management.
You write Markdown documents containing fenced code blocks. Inside those code blocks, noweb-style chunks describe output files and reusable snippets. Reweave optionally expands a strict macro language first, then tangles the chunks and writes the requested files directly.
What It Does
- Reads one or more Markdown files.
- Expands
%macros by default. - Scans code fences for chunk definitions.
- Expands named chunk references.
- Writes every
@filechunk to an output directory — content-aware: files whose content is unchanged are left untouched (mtime preserved), so downstream build tools are not needlessly re-triggered.
Reweave is intentionally forward-only: input Markdown goes in, generated files come out. It does not track provenance, reconcile hand edits in generated files, or maintain persistent state.
Build
Build the workspace:
cargo build
Build an optimized release executable:
cargo build --release --bin reweave
The release binary is written to:
target/release/reweave
Run the test suite:
cargo test --workspace
Run clippy with warnings denied:
cargo clippy --workspace -- -D warnings
Check coverage and fail if line coverage drops below the project floor:
scripts/check-coverage.sh
The default floor is 99.5%. Override it for stricter local checks:
REWEAVE_COVERAGE_MIN=99.7 scripts/check-coverage.sh
Basic Use
Create a Markdown file with chunk definitions in fenced code blocks:
# Example
```rust
// <[@file src/main.rs]>=
fn main() {
// <[body]>
}
// @
// <[body]>=
println!("hello");
// @
```
Generate files:
cargo run --bin reweave -- examples/hello.md --out /tmp/reweave-out
Or use a release binary:
target/release/reweave examples/hello.md --out /tmp/reweave-out
This writes:
/tmp/reweave-out/src/main.rs
CLI
reweave [OPTIONS] [INPUTS]...
Common options:
-o, --out DIR Output directory for @file chunks
--dir DIR Recursively read files under DIR
--ext EXT Extension for --dir discovery, default: md
--no-macro Tangle input without macro expansion
--macro-only Expand macros to stdout without tangling
-D, --define NAME=VALUE Define a top-level macro variable
-I, --include DIR Add a macro include/import search path
--sigil CHAR Change the macro sigil, default: %
--allow-env Enable %env(NAME)
--env-prefix PREFIX Prefix applied to environment lookups
--open-delim TEXT Noweb open delimiter, default: <[
--close-delim TEXT Noweb close delimiter, default: ]>
--chunk-end TEXT Chunk end marker, default: @
--comment-marker TEXT Accepted chunk comment marker, repeatable
--formatter CMD Format every output needing it via `CMD [ARGS...]
<file>...` before writing — one run across the
whole batch, not one per file (shell-word split,
so arguments are allowed, e.g. `--formatter
'clojure -M:cljfmt fix'`); repeatable; runs before
change detection
--stamp FILE Write a stamp file (ninja target) after a run
--depfile FILE Write a Makefile-style depfile (inputs + includes)
--recursion-limit N Macro and chunk expansion recursion limit
Directory mode:
reweave --dir docs --ext md --out generated
Multiple inputs are processed in argument order:
reweave chapters/intro.md chapters/impl.md --out generated
Expand macros without tangling or writing @file chunks:
reweave input.md --macro-only > expanded.md
With multiple inputs, --macro-only writes each expanded document to stdout in
argument order. It cannot be combined with --no-macro.
Using Reweave in a Project
Main documents and fragments
A document that %includes another is a main document; the included file is
a fragment. Feed reweave the whole source set — fragments are handled
natively: reweave discovers includes through its own macro evaluator
(macro-computed include arguments work), splices fragments into the including
documents, and tangles only the main documents. A fragment is never expanded
or tangled standalone, so there are no duplicate-@file errors and no
undefined-macro failures from out-of-scope expansion.
Build system integration
Reweave tangles inputs and writes outputs — no watch mode, no persistent state. Everything a build graph needs is built in:
--stamp FILEwrites a stamp file after a successful run (the ninja target); it lists the outputs that were actually written.--depfile FILEwrites a Makefile-style depfile covering every file that can influence the outputs: all inputs plus every resolved include.- Writes are content-aware: outputs whose content is unchanged keep their mtime, so downstream compile steps are not re-triggered by a no-op regeneration.
--formatter CMD(repeatable) runsCMD [ARGS...] <file>...before the content-aware comparison, so a formatter (e.g.nph, orclojure -M:cljfmt fix) participates in change detection instead of defeating it.CMDis split shell-style, and every output that actually needs (re)formatting this run has its path appended — one process per configured formatter perwrite_filescall, not one per file: the full set of@fileoutputs isn't known until every chunk has been scanned, and a JVM-booting formatter costs the same whether it is handed one path or a hundred. A small cache under<out>/.reweave/(gitignore it) records raw expansions and output hashes, so unchanged outputs skip the formatter entirely — and a hand-edited output is detected by its hash and regenerated, never kept stale.
Example: Meson
examples/build-integration is a complete,
runnable setup distilled from the rompla project: one Meson custom_target
invoking reweave with --stamp/--depfile — no wrapper scripts:
cd examples/build-integration
meson setup build
ninja -C build
Macro strictness gotchas
- Every chunk reference must be defined; reweave does not silently expand
missing chunks to nothing. Define an empty chunk (
# <[name]>=\n# @) for intentional extension points. - Macro variables are lexically scoped: a nested
%defdoes not capture the outer macro's parameters. Declare them explicitly and pass them at the call site:%def(inner, name, ...)then%inner(%(name)). - Literal
%(...)in prose is evaluated. Escape examples as%%(...). - Error messages always carry
file:line:col, so pipe failures through your editor of choice.
Chunk Syntax
Chunk syntax is recognized inside normal text too, but it is intended to be used inside fenced code blocks.
Output File Chunks
An @file chunk writes a generated file relative to --out:
// <[@file src/lib.rs]>=
pub fn answer() -> u32 {
42
}
// @
Output paths must be relative and must not contain ...
Named Chunks
Named chunks collect reusable code:
// <[imports]>=
use std::path::Path;
// @
Reference a named chunk from another chunk:
// <[@file src/main.rs]>=
// <[imports]>
fn main() {}
// @
Named chunks with the same name accumulate in definition order:
// <[body]>=
first();
// @
// <[body]>=
second();
// @
Replacing Chunks
Use @replace to replace earlier definitions:
// <[@replace body]>=
replacement();
// @
For file chunks:
// <[@replace @file src/main.rs]>=
fn main() {}
// @
Reference Modifiers
Modifiers are placed inside a chunk reference before the chunk name:
// <[@reversed rows]>
// <[@compact body]>
// <[@tight body]>
@reversedexpands multiple definitions in reverse order.@compacttrims blank lines from the start and end of the expansion.@tighttrims edge blank lines and removes all blank lines.
Comment Markers
By default, chunk lines may be prefixed with // or #:
# <[@file out.txt]>=
hello
# @
Use --comment-marker to configure accepted markers.
Macro Language
Macros are expanded before tangling unless --no-macro is used. Use
--macro-only to write the expanded Markdown to stdout without tangling. The
default sigil is %.
Variables
Set a variable:
%set(name, Ada)
Read a variable:
Hello %(name)
Define variables from the CLI:
reweave input.md -D name=Ada --out generated
Variables are scoped. Macro parameters are available only while that macro is expanding.
Macro Definitions
Define a macro:
%def(greet, name, Hello %(name)!)
%greet(Ada)
Output:
Hello Ada!
Macro bodies can be blocks:
%def(fn_line, name, %{
fn %(name)() {}
%})
Call arguments are evaluated before the macro body runs. Missing parameters, too many positional arguments, unknown named arguments, or positional arguments after named arguments are errors.
Named arguments are supported:
%def(tag, name, value, <%(name)>%(value)</%(name)>)
%tag(value=hello, name=span)
Redefinition
%def creates a constant binding. Use %redef when a macro is intentionally
replaceable:
%redef(render, old)
%redef(render, new)
%render()
Blocks
Blocks delay parsing of commas and parentheses inside the block:
%def(wrap, body, %{
before
%(body)
after
%})
Tagged blocks are useful when the content itself contains %}:
%def(raw, %tag{
literal content
%tag})
Verbatim blocks use square brackets and are not macro-expanded while lexed:
%[literal %(text)%]
Tagged verbatim blocks are also supported:
%raw[literal %(text)%raw]
Conditionals
%if(condition, then[, else]) treats a non-empty condition as true:
%if(%(name), Hello %(name), Missing name)
With no then branch, true expands to an empty string.
Pattern Matching
%match(value, default, regex1, result1, regex2, result2, ...) evaluates the
first matching branch:
%match(error-404, unknown,
^warn-\d+$, warning,
^error-\d+$, error)
Regex captures are exposed inside the selected result as %(match_1),
%(match_2), and named captures by name:
%match(error-404, no,
%[^(?P<kind>[a-z]+)-(\d+)$%],
%{kind=%(kind), code=%(match_2)%})
Includes And Imports
Include another file and expand its output in place:
%include(header.md)
Import another file for definitions only:
%import(macros.md)
%import is idempotent per run: importing the same resolved path again, even from a different
top-level document processed by the same invocation (as happens when several files under --dir
share a macro fragment), is a no-op rather than redefining its macros and failing. %include
stays as-is — it always re-pastes the file's output at each call site.
Search paths are controlled with -I / --include.
Aliases
%alias(new_name, source_name[, key=value, ...]) creates a replaceable copy of
an existing macro. Named overrides freeze free variables for the alias:
%def(render, msg, [%(level)] %(msg)) # level is a free variable here
%alias(warn, render, level=WARNING) # the alias freezes it
%warn(check this) # => [WARNING] check this
Dynamic Calls
%eval(name[, args...]) calls a macro whose name is computed:
%set(which, greet)
%eval(%(which), Ada)
%include and %import cannot be called indirectly through %eval — file
inclusion must stay statically visible in the source. Calling
%eval(include, ...) (directly or through a computed name) is an error.
Export
%export(name) copies a variable or macro from an inner macro scope to the
enclosing scope. Calling %export at global scope is allowed but produces a
warning.
Environment Variables
Environment access is disabled by default. Enable it with --allow-env:
%env(HOME)
With a prefix:
reweave input.md --allow-env --env-prefix REWEAVE_
Then %env(CONFIG) reads REWEAVE_CONFIG.
String Helpers
%capitalize(text)
%decapitalize(text)
%convert_case(text, snake)
%to_snake_case(text)
%to_camel_case(text)
%to_pascal_case(text)
%to_screaming_case(text)
Supported case names include:
snake
camel
pascal
kebab
screaming
screaming_kebab
ada
lower
upper
Predicates
%eq(a, b)
%neq(a, b)
%not(value)
These return non-empty strings for true and empty strings for false, so they
compose with %if.
Python-Style Script Macros
Reweave also supports %pydef, backed by the embedded Monty evaluator:
%pydef(double, x, %{str(int(x) * 2)%})
%double(21)
Store helpers are available to script macros:
%pyset(counter, 1)
%pyget(counter)
Script parameters shadow store keys with the same name.
Strictness
Reweave deliberately fails on ambiguous or unsafe input:
- Undefined macros and variables are errors.
- Undefined chunks are errors by default.
- Recursive chunk references are detected.
- Macro recursion is bounded by
--recursion-limit. - Output paths must be relative and safe.
%setis not allowed in macro argument position.
Every error carries its exact source position, rendered as
file:line:col: <error> — either the position of the offending construct or,
for errors without their own position, the innermost macro call site they
propagated through:
Error: reweave::macro_expand
× macro expansion failed
╰─▶ docs/main.md:2:1: Undefined variable: missing
Project Layout
reweave-cli Command-line entry point
reweave-macro Strict macro evaluator
reweave-tangle Chunk parser, expander, and file writer
reweave-core Shared constants
examples Small input examples
docs Design notes