No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
giannifer7 0dbb5c935c
fix: make %import idempotent per run
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>
2026-10-20 11:28:26 +02:00
docs feat: let --formatter take arguments, and batch it across a run 2026-10-19 12:18:32 +02:00
examples feat: formatter cache — skip formatting unchanged outputs 2026-09-09 09:26:01 +02:00
reweave-cli feat: allow kebab-case identifiers for macro names, params, and --define 2026-10-20 10:46:40 +02:00
reweave-core Initial Reweave implementation 2026-05-15 18:11:30 +02:00
reweave-macro fix: make %import idempotent per run 2026-10-20 11:28:26 +02:00
reweave-tangle feat: let --formatter take arguments, and batch it across a run 2026-10-19 12:18:32 +02:00
scripts Add macro-only mode and raise coverage 2026-05-16 21:20:58 +02:00
.gitignore Initial Reweave implementation 2026-05-15 18:11:30 +02:00
AGENTS.md docs: usage guide, agent instructions overhaul, drop weaveback references 2026-09-05 18:59:01 +02:00
Cargo.lock feat: let --formatter take arguments, and batch it across a run 2026-10-19 12:18:32 +02:00
Cargo.toml feat: let --formatter take arguments, and batch it across a run 2026-10-19 12:18:32 +02:00
README.md fix: make %import idempotent per run 2026-10-20 11:28:26 +02:00

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 @file chunk 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 FILE writes a stamp file after a successful run (the ninja target); it lists the outputs that were actually written.
  • --depfile FILE writes 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) runs CMD [ARGS...] <file>... before the content-aware comparison, so a formatter (e.g. nph, or clojure -M:cljfmt fix) participates in change detection instead of defeating it. CMD is split shell-style, and every output that actually needs (re)formatting this run has its path appended — one process per configured formatter per write_files call, not one per file: the full set of @file outputs 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 %def does 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]>
  • @reversed expands multiple definitions in reverse order.
  • @compact trims blank lines from the start and end of the expansion.
  • @tight trims 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.
  • %set is 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