Copperlace Capabilities

Copperlace renders text from named rules and can render object-valued rules as structured values. A text render starts from one rule name, expands any template references it encounters, and returns a single string or a RenderError. Structured rendering preserves an object tree while rendering its text leaves. See Structured output for JSON and wrapper examples.

Named Rules

Every top-level key is a renderable rule, except for the special top-level context object. Nested object leaves are available as dotted rule names, so name.first = ["Mia"] can be rendered as {name.first}. Object parents such as name are not directly renderable.

name = ["Mia"]
origin = "Hello {name}"

Rendering origin resolves {name} by rendering the top-level name rule. Rendering name directly is also valid.

Templates

String values are parsed as templates. Literal text is copied into the output. Expressions inside braces are resolved at render time and append text to the output. Statements inside {% …​ %} are resolved at render time but never append text directly.

adjective = ["bright"]
story = "A {adjective} path"
origin = "{story}"

Template expression whitespace is trimmed, so { name } resolves the same rule as {name}.

Processors

Template expressions can pipe rendered output through processors. Processors run left-to-right and transform strings only.

name = ["  mIA  "]
origin = "Hello {name | trim | capitalize}"

Rendering origin produces Hello Mia.

Builtin processors are uppercase, lowercase, trim, capitalize, titlecase, article, past_tense, pluralize, singularize, and possessive, present_participle, ordinal, sentence, quote, and slug. The article processor prefixes rendered text with a or an, as in {item | article}. The past_tense and present_participle processors convert one verb token to past tense or -ing form. The pluralize and singularize processors convert one noun token between singular and plural forms, possessive adds an English possessive suffix, ordinal adds an ordinal suffix to one integer token, sentence capitalizes the first alphabetic character, and quote wraps rendered text in escaped double quotes. The slug processor normalizes rendered text into a lowercase hyphenated slug. Rust, C ABI, Python, Java, and JS/WASM callers can register custom processors when constructing a ruleset. Custom processors extend the builtin registry and override builtin processors when names collide. The CLI uses the builtin processor registry only.

Random Choices

Array values are random choices. Rendering an array-backed rule chooses one child value and renders that value.

mood = [vexed, wistful, astute]
origin = "The path felt {mood}."

Choice elements may be strings, arrays, or scalar values. Nested object values are not renderable outside the special top-level context object, except for weighted choice entries inside arrays.

Weighted choices use object entries with value and weight. If any array entry is weighted, plain entries in the same array use weight 1.0.

mood = [
  { value = common, weight = 9.5 },
  { value = rare, weight = 0.5 },
  ordinary
]

Arrays inside structured object-valued rules are different from top-level list rules: they render as arrays and preserve their children in order. Top-level list rules remain random text choices in v1.

Arrays inside structured objects are preserved for structured rendering. Nested arrays that are valid structured data but not valid weighted text choices are accepted structurally and rejected only if callers try to render that nested array as a dotted text rule.

Bindings

Copperlace supports two binding forms. Both forms emit no text directly.

Bind If Missing

A statement of the form {% alias:rule %} ensures alias has a value for the current render and emits no text directly. If alias is missing, Copperlace renders rule and stores the result under alias. If alias already exists, the statement leaves the existing value unchanged and does not render rule.

name = ["Mia"]
origin = "{% hero:name %}{hero}/{hero}"

Rendering origin produces Mia/Mia. The first statement binds hero; later {hero} references reuse the same value.

Bindings do not overwrite an existing value:

first = ["Mia"]
second = ["Darcy"]
origin = "{% hero:first %}{% hero:second %}{hero}"

Rendering origin produces Mia. The second binding statement is a no-op because hero already exists.

Binding statements store processed values when a pipeline is present:

name = ["mia"]
origin = "{% hero:name | uppercase %}{hero}"

Rendering origin produces MIA.

Overwrite Binding

A statement of the form {% alias:=rule %} always renders rule, stores the result under alias, and replaces any existing value for alias.

first = ["Mia"]
second = ["Darcy"]
origin = "{% hero:first %}{% hero:=second %}{hero}"

Rendering origin produces Darcy. The overwrite binding replaces the previous hero value.

Bindings are scoped to one call to render_rule. A later render starts with an empty render context and may choose different random values.

Context Defaults

A top-level context object defines lazy defaults for names used inside templates. When a template references {name}, resolution uses this order:

  1. Existing value already bound in the current render context.

  2. Lazy default from top-level context.

  3. Top-level rule with the same name.

name = ["Mia"]
animal = ["owl"]
story = "{hero} and {heroPet}"
origin = "{story}"

context = {
  hero = "{name}"
  heroPet = "{animal}"
}

Rendering origin renders story; the first {hero} reference renders and caches context.hero, and the first {heroPet} reference renders and caches context.heroPet.

Rust callers can also provide initial render context values when rendering a rule. Initial values use the same resolution slot as existing bindings: they resolve before lazy context defaults and named rules, and they are scoped to one render call.

Cycle Detection

Copperlace tracks the rule call stack during a render. If a rule attempts to render a rule already on the stack, rendering fails with CircularRuleReference.

a = "{b}"
b = "{a}"