copperlace
v0.3.1
Published
Procedural text renderer for generative grammar configuration
Maintainers
Readme
= Copperlace
Copperlace turns generative grammars into varied text.
Write the pieces once, then ask Copperlace to render a named rule. It can pick
random options, remember a picked value for the rest of one result, fill in
default values, and clean up text with processors such as article,
possessive, pluralize, and sentence.
Use it for story snippets, game item descriptions, product blurbs, release notes, quest logs, or any place where hand-written text needs controlled variation.
== What It Does
Here is a tiny Copperlace configuration:
[source]
name = ["Mia", "Lina"] animal = ["owl", "raven"] mood = ["quiet", "restless"]
story = "{hero} traveled with {pet | article}. {hero} stayed {mood}." origin = "{% hero:name %}{% pet:animal %}{story}"
Rendering origin might produce:
[source,text]
Mia traveled with an owl. Mia stayed quiet.
Another render might produce:
[source,text]
Lina traveled with a raven. Lina stayed restless.
The important detail is consistency inside one result: {% hero:name %} chooses a
name once, and later {hero} references reuse that same name.
When you need different entries from the same choice rule, add ! to draw
without replacement for the current render:
[source]
name = ["Mia", "Lina"] duel = "{name!} faces {name!}"
Template expressions also support processor pipelines:
[source]
item = ["ancient key", "owl feather", "brass compass"] slug = "{item | slug}" description = "You found {item | article}."
This can render text like:
[source,text]
You found an ancient key.
The article processor chooses a or an, and slug turns text into a
URL-friendly value such as ancient-key.
== License
Copyright 2026 Bruno Mahé.
Copperlace is licensed under the Apache License, Version 2.0. See link:LICENSE[LICENSE] for details.
== Quick Start
Run the bundled example from the Rust project:
[source,sh]
cd rust-core cargo run --bin copperlace -- render -c ../examples/character_scene.conf
Example output:
[source,text]
Darcy's scholar found a brass compass in a moonlit bridge while running. The quiet discovery changed everything.
Your exact result may differ because the example contains random choices.
Render three results from the same loaded configuration:
[source,sh]
cargo run --bin copperlace -- render -c ../examples/character_scene.conf -n 3
Example output:
[source,text]
Mia's ranger found an hourglass in a forgotten observatory while searching. The quiet discovery changed everything. Azra's ranger found a brass compass in a moonlit bridge while searching. The restless discovery changed everything. Lina's scholar found an owl feather in a moonlit bridge while watching. The restless discovery changed everything.
Provide a starting value with --set. Here the hero is fixed to Mia, while
the other values can still vary:
[source,sh]
cargo run --bin copperlace -- render -c ../examples/character_scene.conf --set hero=Mia
Example output:
[source,text]
Mia's pilot found a brass compass in a forgotten observatory while running. The quiet discovery changed everything.
Check a configuration without rendering it:
[source,sh]
cargo run --bin copperlace -- check -c ../examples/character_scene.conf
Output:
[source,text]
OK
Use - as the config path to read configuration from standard input:
[source,sh]
cat ../examples/character_scene.conf | cargo run --bin copperlace -- render -c - cat ../examples/character_scene.conf | cargo run --bin copperlace -- check -c -
More runnable examples live in link:examples/README.adoc[examples/], covering story text, game item cards, gendered pronouns, JSON item data, structured item data, product copy, release notes, and quest logs.
Object-valued rules render as structured JSON. The CLI formats structured JSON with tabs by default:
[source,sh]
cargo run --bin copperlace -- render -c ../examples/structured_item.conf
Example output:
[source,json]
{ "description": "A moonstone compass once saved a lost caravan. It still releases 3 sparks when its bearer is searching in the dark.", "id": "rare-moonstone-compass-2nd", "level": "2", "name": "Rare Moonstone Compass", "properties": { "charges": "3", "effect": "spark", "material": "moonstone" }, "rarity": "rare", "tags": [ "rare", "compass", "spark" ], "type": "compass" }
Use --compact-json when one-line JSON is more convenient.
For development checks, run:
[source,sh]
make check
== Load Once, Render Many Times
For repeated renders, load the configuration once and call render multiple
times. This avoids recompiling the rule set for each render.
Copperlace is available as a command-line tool and as libraries for Rust, Python, Java, and browser JavaScript/WebAssembly.
=== Rust
[source,rust]
use copperlace::Copperlace;
let copperlace = Copperlace::from_file("examples/character_scene.conf")?; let first = copperlace.render("origin")?; let second = copperlace.render("origin")?; let seeded = copperlace.render_with_context( "origin", [("hero".to_string(), "Mia".to_string())].into(), )?;
One-shot helpers are also available when repeated rendering is not needed.
=== Python
[source,python]
from copperlace import Copperlace
with Copperlace.from_file("examples/character_scene.conf") as copperlace: first = copperlace.render("origin") second = copperlace.render("origin") seeded = copperlace.render("origin", {"hero": "Mia"})
with Copperlace.from_string( 'name = ["Mia"]\norigin = "{name | shout}"', {"shout": lambda value: value.upper()}, ) as copperlace: shouted = copperlace.render("origin")
Python also exposes RuleSet and one-shot render helpers. Wrapper errors are
raised as CopperlaceError.
=== Java
[source,java]
import dev.mahe.copperlace.Copperlace; import java.util.Map;
try (final Copperlace copperlace = Copperlace.fromFile("examples/character_scene.conf")) { final String first = copperlace.render("origin"); final String second = copperlace.render("origin"); final String seeded = copperlace.render("origin", Map.of("hero", "Mia")); }
try (final Copperlace copperlace = Copperlace.fromStringWithProcessors( "name = ["Mia"]\norigin = "{name | shout}"", Map.of("shout", value -> value.toUpperCase()))) { final String shouted = copperlace.render("origin"); }
Java also exposes RuleSet and one-shot render helpers. Copperlace and
RuleSet implement AutoCloseable.
=== JS/TS
[source,ts]
import { Copperlace } from "./pkg/copperlace";
const response = await fetch("/config.conf"); const config = await response.text(); const copperlace = new Copperlace(config); const output = copperlace.render("origin"); const seeded = copperlace.renderWithContext("origin", { hero: "Mia" }); const custom = Copperlace.withProcessors(config, { shout: (value) => value.toUpperCase(), }).render("origin");
The JS/TS package is generated with wasm-pack. Bundler output can be imported
directly; direct browser ES module output must call the generated init
function before using Copperlace. Browser code should load config text, then
pass the string to new Copperlace(config). File-path APIs are not exported to
WebAssembly.
== Documentation
The detailed behavior and configuration contract lives in docs/:
- link:docs/index.adoc[Documentation index]
- link:docs/capabilities.adoc[Capabilities]
- link:docs/configuration.adoc[Configuration]
- link:docs/errors.adoc[Errors and API behavior]
- link:docs/packaging.adoc[Packaging and language wrappers]
== Project Layout
rust-core/contains the renderer library, C ABI, and CLI.python/contains the Python wheel wrapper andctypesbindings.java/contains the Java FFM wrapper and Maven packaging.js/contains JS/TS WebAssembly package documentation.docs/contains the behavior and configuration specification.examples/contains runnable rule examples.
== Development Commands
Use the root Makefile for normal workflows:
[source,sh]
make help # list available targets make check # Rust fmt check plus Rust, Python, and Java tests make test # Rust, Python, and Java tests make package # Python wheel, JS/TS wasm package, and Java JARs make clean # remove generated Rust, Python, JS/TS, and Java build outputs
Common targeted commands:
[source,sh]
make rust-build make rust-test make rust-cli make python-test make python-wheel make js-package make js-web make java-test make java-package
== Packaging Notes
Python wheels and Java native JARs are platform-specific because they include
the Rust dynamic library. Python source distributions include the Rust sources
needed to build that native library locally. JS/TS packages are generated from
the same Rust renderer as WebAssembly and require wasm-pack.
The simplest Java dependency is dev.mahe.copperlace:copperlace, which brings
in the Java API plus all supported native platform artifacts. Users who only
want one runtime platform can depend on a platform artifact such as
dev.mahe.copperlace:copperlace-linux-x86_64, and advanced users can depend on
dev.mahe.copperlace:copperlace-api directly. At runtime, the Java wrapper
loads the native library from COPPERLACE_LIBRARY_PATH first, then from
packaged native resources, then from local Rust build output for source-tree
development.
