Configuring the Render Environment

An Environment holds the choices that apply to a family of layouts: where to find them, which filters they may use, and how their syntax and output should behave. Create and configure it once, then render many documents with separate local contexts. This page shows how shared values and local values meet, and how to choose the environment options for your layouts.

Environment Reuse, Concurrent Rendering, and Auto-Reload

The environment is the entry point for render(). It takes a logical layout name, finds the source through its registered loaders, and returns a complete String. Register loaders and filters during setup, before the first render. After setup, the same environment can be used by concurrent render calls, provided application filters and custom loaders also support concurrent use. The Using the Render Engine page shows that basic sequence with one file-backed layout.

When layout files change during development, enableAutoReload() asks loaders to check them again on each render. This is useful for a live editing cycle, although it costs extra lookup work. Enable it during setup; once enabled, it stays on for that environment. For a deployed set of stable layouts, the ordinary cached behavior avoids those checks.

Global Render Context and Shared Values

Suppose several observations share the same current and coastline. Place those values in a global Context and render without passing a local context. setGlobalContext() replaces the whole global context with a new snapshot, so later renders can see updated shared values.

text/RenderLayouts/EnvironmentExamples.cpp

void sharedValues() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "current.txt"_el)
        .content()
        .writeTextOrThrow("Corrente: {{ current }}; costa: {{ coast }}\n"_el);

    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    environment->setGlobalContext(el::render::Context{}.set("current"_el, "Ligure"_el).set("coast"_el, "Italia"_el));
    el::io::print(environment->render("current.txt"_el));
    environment->setGlobalContext(
        el::render::Context{}.set("current"_el, "Adriatica"_el).set("coast"_el, "Croazia"_el));
    el::io::print(environment->render("current.txt"_el));
}

Output

Corrente: Ligure; costa: Italia
Corrente: Adriatica; costa: Croazia

A global context works well for values that genuinely belong to every document in this environment, such as a common site title or a set of stable labels. Replacing the snapshot is also safe while other threads render: a render call uses one consistent snapshot. If a value differs for each document, pass it locally instead.

Local Context Overrides of Global Values

A local context belongs to one render() call. A name found there wins over the same name in the global snapshot; names absent locally still come from the global context. Here the local station replaces the shared station, while season remains shared.

text/RenderLayouts/EnvironmentExamples.cpp

void localValues() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "observation.txt"_el)
        .content()
        .writeTextOrThrow("{{ station }}: {{ current }} ({{ season }})\n"_el);

    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    environment->setGlobalContext(
        el::render::Context{}.set("station"_el, "Punto nord"_el).set("season"_el, "estate"_el));
    const auto local = el::render::Context{}.set("station"_el, "Punto sud"_el).set("current"_el, "Ligure"_el);
    el::io::print(environment->render("observation.txt"_el, local));
}

Output

Punto sud: Ligure (estate)

This separation lets you keep broad defaults in one place without making one document’s values affect another. If a name is absent from both contexts, it resolves to null and inserts empty text. The rendering reference lists the value types and lookup rules.

Registering Custom Render Filters

A layout may need a small transformation that belongs to your application rather than to the layout language. Register a named FilterFn with addFilter(). The callback receives a ValueList: element zero is the piped value, and up to two further elements are positional arguments from the layout.

text/RenderLayouts/EnvironmentExamples.cpp

void customFilter() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "current.txt"_el).content().writeTextOrThrow("{{ current | label('Corrente: ') }}\n"_el);

    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    environment->addFilter("label"_el, [](const el::render::ValueList &values) -> el::render::Value {
        if (values.count() != ItemCount{2U} || !values.getRef(ItemIndex{0U}).isText() ||
            !values.getRef(ItemIndex{1U}).isText()) {
            throw el::render::RenderError{el::render::RenderErrorContext{
                el::render::RenderErrorCategory::Runtime,
                "Invalid label filter arguments"_el,
                "The label filter needs text and one text prefix."_el}};
        }
        return el::StringList{values.getRef(ItemIndex{1U}).asText(), values.getRef(ItemIndex{0U}).asText()}.join();
    });
    el::io::print(environment->render("current.txt"_el, el::render::Context{}.set("current"_el, "Ligure"_el)));
}

Output

Corrente: Ligure

Validate the count and types inside the callback, as the demo does, before using a value.

These arguments can come from user-supplied layouts and context data; prefer RenderError with category Runtime to reject invalid values. Supply a title and description and leave the source context unset, as the demo does.

Filter names are ASCII identifiers, and each application filter name is registered once. The output modifiers escape, e, and safe have reserved names. A list of the built-in filters and output modifiers appears in the Template Feature Cheat Sheet. A registered filter may run on several threads at the same time; any mutable state captured by its callback needs its own synchronization.

Configuring Layout Delimiters

EnvironmentOptions is passed to Environment::create(). Without custom options, expressions use {{ ... }}, statements use {% ... %}, and comments use {# ... #}. Change these choices before creating the environment, then write every layout for that environment with the chosen syntax.

Expression Delimiters

setExpressionDelimiters() changes how inserted values are marked. This matters when the surrounding document format already uses braces. The example below uses [[ current ]]; expressionDelimiters() returns the configured pair.

Statement Delimiters

setStatementDelimiters() changes control statements such as if, for, and include. The example uses <% if active %>. statementDelimiters() returns that pair.

Comment Delimiters

setCommentDelimiters() chooses text that the renderer discards without evaluating it. The example’s (# note #) is absent from the result. commentDelimiters() returns this pair.

The three settings work together in one layout:

text/RenderLayouts/EnvironmentExamples.cpp

void customSyntax() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "current.txt"_el)
        .content()
        .writeTextOrThrow("(# note #)<% if active %>[[ current ]]<% endif %>\n"_el);

    auto options = el::render::EnvironmentOptions{};
    options.setExpressionDelimiters(el::render::Delimiters{"[["_el, "]]"_el})
        .setStatementDelimiters(el::render::Delimiters{"<%"_el, "%>"_el})
        .setCommentDelimiters(el::render::Delimiters{"(#"_el, "#)"_el});
    const auto environment = el::render::Environment::create(options);
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    el::io::print(environment->render(
        "current.txt"_el, el::render::Context{}.set("active"_el, true).set("current"_el, "Ligure"_el)));
}

Output

Ligure

Each setting takes a Delimiters pair with begin and end markers. A delimiter can also carry an optional line marker for line statements. Choose distinct markers that do not collide with ordinary source text or one another.

Configuring Output Escaping

Automatic Escaping

By default, inserted values are escaped according to the layout name’s suffix. For example, .html selects HTML escaping; .xml, .json, .md, and .elcl have their own default mappings. A suffix without a mapping produces unescaped output. automaticEscapingEnabled() reports whether this feature is on, and setAutomaticEscapingEnabled(false) switches it off for the entire environment. The third render in the example below turns off automatic escaping for a second environment and shows the original ampersand. This broad switch is appropriate only when every layout in that environment is intended to emit trusted, unescaped text.

Escape Formats for Suffixes

A more focused choice is setEscapeFormatForSuffix(). The renderer picks the longest matching suffix, so .raw.html can take precedence over .html. The example keeps ordinary HTML escaping and gives one specially named layout an EscapeFormat of None.

text/RenderLayouts/EnvironmentExamples.cpp

void escapingOptions() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "note.html"_el).content().writeTextOrThrow("{{ title }}\n"_el);
    (directory->path() / "note.raw.html"_el).content().writeTextOrThrow("{{ title }}\n"_el);

    auto options = el::render::EnvironmentOptions{};
    options.setEscapeFormatForSuffix(".raw.html"_el, el::EscapeFormat::None);
    const auto environment = el::render::Environment::create(options);
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    const auto context = el::render::Context{}.set("title"_el, "Mare & vento"_el);
    el::io::print(environment->render("note.html"_el, context));
    el::io::print(environment->render("note.raw.html"_el, context));

    auto plainOptions = el::render::EnvironmentOptions{};
    plainOptions.setAutomaticEscapingEnabled(false);
    const auto plainEnvironment = el::render::Environment::create(plainOptions);
    plainEnvironment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    el::io::print(plainEnvironment->render("note.html"_el, context));
}

Output

Mare &amp; vento
Mare & vento
Mare & vento

escapeFormatForLayout() lets you inspect the choice for a logical name. removeEscapeFormatForSuffix() removes one assignment, while clearEscapeFormats() removes all assignments, including the defaults. A suffix begins with . and uses ASCII letters, digits, ., _, or -. Keep ordinary automatic escaping in place for documents that may contain values from outside your application.

Rendering Work and Output Limits

A layout can generate much more text than its source contains, especially with loops and includes. setRenderLimits() replaces the RenderLimits used by the environment; renderLimits() reads the current limits. The following example sets a deliberately tiny output limit to show the resulting error boundary.

text/RenderLayouts/EnvironmentExamples.cpp

void renderingLimit() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "current.txt"_el).content().writeTextOrThrow("Corrente Ligure"_el);

    auto limits = el::render::RenderLimits{};
    limits.setGeneratedOutputBytes(8U);
    auto options = el::render::EnvironmentOptions{};
    options.setRenderLimits(limits);
    const auto environment = el::render::Environment::create(options);
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    try {
        el::io::print(environment->render("current.txt"_el));
    } catch (const el::render::RenderError &) {
        el::io::printLine("The output limit was reached."_el);
    }
}

Output

The output limit was reached.

The limit object also controls executed instructions, runtime nesting, callback resolution depth, lexical scopes, call frames, value stack depth, and static dependency depth. The defaults fit ordinary documents; adjust a limit when your expected layout complexity calls for it. A limit failure raises RenderError, just like other render failures.

Once the environment is configured, Loading Render Layouts shows how its loaders find files, compiled resources, and application-owned sources.

Complete demo sources

text/RenderLayouts/EnvironmentExamples.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include <DemoCommon.hpp>
#include <erbsland/conf/ValueList.hpp>
#include <erbsland/EscapeFormat.hpp>
#include <erbsland/ItemCount.hpp>
#include <erbsland/ItemIndex.hpp>
#include <erbsland/Path.hpp>
#include <erbsland/PathContent.hpp>
#include <erbsland/PathOperations.hpp>
#include <erbsland/render/Context.hpp>
#include <erbsland/render/Delimiters.hpp>
#include <erbsland/render/Environment.hpp>
#include <erbsland/render/EnvironmentOptions.hpp>
#include <erbsland/render/FileSystemLoader.hpp>
#include <erbsland/render/RenderError.hpp>
#include <erbsland/render/RenderErrorCategory.hpp>
#include <erbsland/render/RenderErrorContext.hpp>
#include <erbsland/render/RenderLimits.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/StringList.hpp>
#include <erbsland/TempDirectory.hpp>

namespace demo {

using namespace el::unit;

/// Share stable values across renders through the environment's global context.
///
/// A global context supplies names to every layout rendered by the environment. Replacing it publishes a new
/// snapshot for subsequent calls without changing the layout or the loader.
/// @notest{Demo function verified by the documentation executable.}
void sharedValues() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "current.txt"_el)
        .content()
        .writeTextOrThrow("Corrente: {{ current }}; costa: {{ coast }}\n"_el);

    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    environment->setGlobalContext(el::render::Context{}.set("current"_el, "Ligure"_el).set("coast"_el, "Italia"_el));
    el::io::print(environment->render("current.txt"_el));
    environment->setGlobalContext(
        el::render::Context{}.set("current"_el, "Adriatica"_el).set("coast"_el, "Croazia"_el));
    el::io::print(environment->render("current.txt"_el));
}

/// Give one render its own values while retaining shared defaults.
///
/// A local name wins when it is also present in the global context. Other global names remain available to
/// the layout, so a caller only supplies the values that vary for one document.
/// @notest{Demo function verified by the documentation executable.}
void localValues() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "observation.txt"_el)
        .content()
        .writeTextOrThrow("{{ station }}: {{ current }} ({{ season }})\n"_el);

    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    environment->setGlobalContext(
        el::render::Context{}.set("station"_el, "Punto nord"_el).set("season"_el, "estate"_el));
    const auto local = el::render::Context{}.set("station"_el, "Punto sud"_el).set("current"_el, "Ligure"_el);
    el::io::print(environment->render("observation.txt"_el, local));
}

/// Add one application filter for a value transformation used by a layout.
///
/// A filter receives the piped value at index zero and any positional arguments after it. Validate the
/// arguments in the callback before reading their concrete types. Reject invalid template arguments with
/// RenderError; the renderer supplies the filter's source location.
/// @notest{Demo function verified by the documentation executable.}
void customFilter() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "current.txt"_el).content().writeTextOrThrow("{{ current | label('Corrente: ') }}\n"_el);

    const auto environment = el::render::Environment::create();
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    environment->addFilter("label"_el, [](const el::render::ValueList &values) -> el::render::Value {
        if (values.count() != ItemCount{2U} || !values.getRef(ItemIndex{0U}).isText() ||
            !values.getRef(ItemIndex{1U}).isText()) {
            throw el::render::RenderError{el::render::RenderErrorContext{
                el::render::RenderErrorCategory::Runtime,
                "Invalid label filter arguments"_el,
                "The label filter needs text and one text prefix."_el}};
        }
        return el::StringList{values.getRef(ItemIndex{1U}).asText(), values.getRef(ItemIndex{0U}).asText()}.join();
    });
    el::io::print(environment->render("current.txt"_el, el::render::Context{}.set("current"_el, "Ligure"_el)));
}

/// Choose delimiters that fit the text format containing a layout.
///
/// Expression, statement, and comment delimiters are configured together before creating the environment.
/// @notest{Demo function verified by the documentation executable.}
void customSyntax() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "current.txt"_el)
        .content()
        .writeTextOrThrow("(# note #)<% if active %>[[ current ]]<% endif %>\n"_el);

    auto options = el::render::EnvironmentOptions{};
    options.setExpressionDelimiters(el::render::Delimiters{"[["_el, "]]"_el})
        .setStatementDelimiters(el::render::Delimiters{"<%"_el, "%>"_el})
        .setCommentDelimiters(el::render::Delimiters{"(#"_el, "#)"_el});
    const auto environment = el::render::Environment::create(options);
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    el::io::print(environment->render(
        "current.txt"_el, el::render::Context{}.set("active"_el, true).set("current"_el, "Ligure"_el)));
}

/// Configure escaping for layouts whose suffix identifies the output format.
///
/// Built-in suffix mappings cover common formats. A longer custom suffix can override one mapping; automatic
/// escaping can also be disabled when a trusted plain-text output needs its original characters.
/// @notest{Demo function verified by the documentation executable.}
void escapingOptions() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "note.html"_el).content().writeTextOrThrow("{{ title }}\n"_el);
    (directory->path() / "note.raw.html"_el).content().writeTextOrThrow("{{ title }}\n"_el);

    auto options = el::render::EnvironmentOptions{};
    options.setEscapeFormatForSuffix(".raw.html"_el, el::EscapeFormat::None);
    const auto environment = el::render::Environment::create(options);
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    const auto context = el::render::Context{}.set("title"_el, "Mare & vento"_el);
    el::io::print(environment->render("note.html"_el, context));
    el::io::print(environment->render("note.raw.html"_el, context));

    auto plainOptions = el::render::EnvironmentOptions{};
    plainOptions.setAutomaticEscapingEnabled(false);
    const auto plainEnvironment = el::render::Environment::create(plainOptions);
    plainEnvironment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    el::io::print(plainEnvironment->render("note.html"_el, context));
}

/// Bound the amount of text a layout may generate.
///
/// Limits are chosen when the environment is created. A limit failure is a RenderError, so callers can use
/// the same error boundary as for loading and syntax failures.
/// @notest{Demo function verified by the documentation executable.}
void renderingLimit() {
    const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
    (directory->path() / "current.txt"_el).content().writeTextOrThrow("Corrente Ligure"_el);

    auto limits = el::render::RenderLimits{};
    limits.setGeneratedOutputBytes(8U);
    auto options = el::render::EnvironmentOptions{};
    options.setRenderLimits(limits);
    const auto environment = el::render::Environment::create(options);
    environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
    try {
        el::io::print(environment->render("current.txt"_el));
    } catch (const el::render::RenderError &) {
        el::io::printLine("The output limit was reached."_el);
    }
}

}