Transforming Placeholder Text with Built-in Filters
Placeholder sources often return text that is technically valid but not yet suitable for its destination. It may contain surrounding whitespace, an application prefix, a long diagnostic label, or markup characters that need escaping. The built-in text filters handle these common transformations directly in a placeholder expression and can also select or reject a value based on a small condition.
Enabling the Filter Set
Call Replacer::addTextFilters() before replacing
text that uses the built-in filters.
This registers safe, trim, slice, remove, replace, escape, if, error_if, default,
required, lower, and upper on that replacer.
You still need at least one placeholder source, such as the Using Built-in Placeholder Sources env source, to begin each
expression.
Filters run from left to right, so each filter receives the result of the previous one.
In |trim|remove:text=-draft|safe:40, the value is first trimmed, then has every exact -draft removed, and only
then is converted into a bounded safe representation.
Reading Filter Parameters
Parameters follow the filter name after a colon. Most filters use named entries separated by commas, with an equals sign between a key and its value:
${var:result_label|slice:start=4,length=12}
Parameter keys and enumerated values such as front or html are case-insensitive.
Text used for searching, replacement, and conditional results preserves its spelling and remains case-sensitive.
The start and length keys have the compact aliases s and l; a numeric alias can omit the equals sign, as
in s4,l12.
Indexes are zero-based, and both indexes and lengths count Unicode code points rather than UTF-8 bytes.
The placeholder syntax still frames the complete parameter. If a pipe or closing brace belongs to parameter text, escape it using the chosen mode described in Choosing Placeholder Syntax for Your Text. A comma separates named entries and therefore cannot be used as literal text in this parameter form.
Cleaning and Selecting Text
The first group of filters cleans a value or selects a part of it. This demo uses a volcanic field note to show a left-to-right chain, a code-point slice, range removal, and a bounded safe string.
text/Placeholders/BuiltInFilters.cpp
void transformFieldNote() {
auto replacer = el::placeholder::Replacer{};
replacer.setVariableSource(
{{{"site"_el, u8" Vulkan_Askja-draft "_el},
{"sample"_el, "obs-2026-basalt"_el},
{"description"_el, u8"Vulkansk aktivitet ved Askja"_el},
{"status"_el, ""_el}}});
replacer.addTextFilters();
el::io::printLine(
replacer.replaceOrThrow("Site: ${var:site|trim|remove:text=-draft|replace:text=_,rep=-|lower}"_el));
el::io::printLine(replacer.replaceOrThrow("Year: ${var:sample|slice:start=4,length=4}"_el));
el::io::printLine(replacer.replaceOrThrow("Sample: ${var:sample|remove:side=front,length=9|upper:ascii}"_el));
el::io::printLine(replacer.replaceOrThrow("Summary: ${var:description|safe:length=18}"_el));
el::io::printLine(replacer.replaceOrThrow("Status: ${var:status|default:pending}"_el));
}
Output
Site: vulkan-askja
Year: 2026
Sample: BASALT
Summary: "Vulka…(28 total)"
Status: pending
Creating Bounded Safe Text
safe converts the current value with String::toSafeString().
It creates a display-oriented representation that escapes unsafe characters and crops long text without splitting an
escape sequence.
The default maximum is 1024 rendered code points.
Parameter |
Meaning |
Example |
|---|---|---|
|
Limit the rendered result to |
|
|
Positional shorthand for the maximum length. |
|
Only one length form may be present. The result follows the default safe-string policy, including automatic quotes and a crop annotation when needed.
Trimming the Ends
trim removes ASCII whitespace from both ends by default.
You can limit trimming to one end or select another set of characters.
Parameter |
Meaning |
Example |
|---|---|---|
|
Trim both ends. This is the default. |
|
|
Trim only the beginning. |
|
|
Trim only the end. |
|
|
Trim characters described by a |
|
side and chars may be combined.
The pattern inside the brackets uses the character-set syntax described in
Working with Character Sets.
Keeping a Slice
slice keeps a code-point range and discards the rest.
The direct range form is the most useful when a field contains a known prefix or fixed-width component.
The side forms make it convenient to retain text relative to either end or a split point.
Parameters |
Result |
Example |
|---|---|---|
|
Keep everything from index |
|
|
Keep at most |
|
|
Keep the first |
|
|
Keep the last |
|
|
Keep the text before split index |
|
|
Keep the text from split index |
|
Without side, start is required and length is optional.
With side, choose front or back and specify exactly one of start or length.
Removing Text
remove can delete a code-point range, every exact text match, or every character in a character set.
Choose one of these modes for each filter invocation.
Parameters |
Result |
Example |
|---|---|---|
|
Remove every exact, case-sensitive occurrence. |
|
|
Remove every character in the pattern set. |
|
|
Remove everything from index |
|
|
Remove at most |
|
|
Remove the first |
|
|
Remove the last |
|
|
Remove the text before split index |
|
|
Remove the text from split index |
|
The same range rules used by slice apply to the range-removal forms.
text and chars cannot be combined with another parameter.
Replacing Exact Text
replace replaces every exact, case-sensitive occurrence of text with rep.
Both parameters are required, and no other parameter is accepted:
Parameter |
Meaning |
Example |
|---|---|---|
|
Text to find. |
|
|
Replacement inserted for every match. |
|
For example, replace:text=_,rep=- turns wave_energy into wave-energy.
Supplying Defaults and Requiring Values
default:<text> returns the current value when it is not empty and returns <text> otherwise:
Region: ${var:deployment_region|default:local}
The default parameter is literal placeholder text and may contain commas.
It is only selected for empty text; the env source’s missing marker undefined is not empty.
Here, deployment_region must exist in the application variable map.
An empty mapped value selects local; an unknown var name remains an error.
required accepts no parameter.
It returns non-empty text unchanged and raises a Validation replacer error for empty text:
Token: ${env:APPLICATION_TOKEN,required|required}
In this example, the source flag rejects a missing environment variable and the filter rejects an existing variable whose value is empty.
Converting Letter Case
lower and upper convert letter case.
Without a parameter, both use Unicode simple case conversion.
The optional mode unicode selects that behavior explicitly, while ascii changes only ASCII letters:
${var:display_name|lower}
${var:protocol|upper:ascii}
Characters without a simple one-code-point mapping remain unchanged.
An unknown mode produces a Syntax replacer error.
Escaping, Choosing, and Validating
The remaining filters prepare text for another syntax or make a small decision from its content. The next demo escapes an HTML fragment, chooses an audience from a review state, and verifies a sample identifier.
text/Placeholders/BuiltInFilters.cpp
void chooseFieldNoteText() {
auto replacer = el::placeholder::Replacer{};
replacer.setVariableSource(
{{{"description"_el, u8"<krater>Askja</krater>"_el}, {"state"_el, "reviewed"_el}, {"sample"_el, "A-104"_el}}});
replacer.addTextFilters();
el::io::printLine(replacer.replaceOrThrow("HTML: ${var:description|escape:format=html,amount=required}"_el));
el::io::printLine(
replacer.replaceOrThrow("Audience: ${var:state|if:contains=review,then=internal,else=public}"_el));
el::io::printLine(replacer.replaceOrThrow("Sample: ${var:sample|error_if:empty}"_el));
}
Output
HTML: <krater>Askja</krater>
Audience: internal
Sample: A-104
Escaping for a Target Syntax
escape converts the current value with String::toEscaped().
The required format names the destination syntax, while the optional amount controls how broadly characters are
escaped.
Parameter |
Accepted values |
Default |
|---|---|---|
|
|
Required |
|
|
|
The result is escaped text; the filter does not add the surrounding quotes or container required by the destination syntax.
Testing a Condition
if and error_if share the same conditions.
Exactly one condition must be present, and lengths count Unicode code points.
Condition |
True when |
Example |
|---|---|---|
|
The current text is empty. This condition has no value. |
|
|
The current text contains an exact, case-sensitive match. |
|
|
The current text has exactly |
|
|
The current text has more than |
|
|
The current text has fewer than |
|
if requires then and else in addition to the condition.
It returns the selected parameter text rather than the original value:
Audience: ${var:review_state|if:contains=review,then=internal,else=public}
error_if accepts only the condition.
It returns the original value when the condition is false and raises a Validation replacer error when the condition
is true.
With strict replacement, the error includes the code-point offset of the expression in the original text.
The following demo uses the simpler required filter to reject an empty application value.
text/Placeholders/BuiltInFilters.cpp
void requireFieldNoteValue() {
auto replacer = el::placeholder::Replacer{};
replacer.setVariableSource({{{"sample"_el, ""_el}}});
replacer.addTextFilters();
try {
[[maybe_unused]] const auto result = replacer.replaceOrThrow("Sample: ${var:sample|required}"_el);
} catch (const el::placeholder::ReplacerError &error) {
el::io::printLine("Required sample failed: "_el, error.reason());
}
}
Output
Required sample failed: A required placeholder value is empty.
Parameter mistakes, such as combining two conditions or omitting a required key, produce a Syntax replacer error.
Calling addTextFilters() twice, or registering a custom filter under one of the built-in names, raises
LogicFailure because filter names must be unique.
Complete demo sources
text/Placeholders/BuiltInFilters.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include "PlaceholderDemos.hpp"
#include <erbsland/placeholder/Replacer.hpp>
#include <erbsland/placeholder/ReplacerError.hpp>
#include <erbsland/StandardStreams.hpp>
namespace demo {
/// Clean and select values in a volcanic field note.
void transformFieldNote() {
auto replacer = el::placeholder::Replacer{};
replacer.setVariableSource(
{{{"site"_el, u8" Vulkan_Askja-draft "_el},
{"sample"_el, "obs-2026-basalt"_el},
{"description"_el, u8"Vulkansk aktivitet ved Askja"_el},
{"status"_el, ""_el}}});
replacer.addTextFilters();
el::io::printLine(
replacer.replaceOrThrow("Site: ${var:site|trim|remove:text=-draft|replace:text=_,rep=-|lower}"_el));
el::io::printLine(replacer.replaceOrThrow("Year: ${var:sample|slice:start=4,length=4}"_el));
el::io::printLine(replacer.replaceOrThrow("Sample: ${var:sample|remove:side=front,length=9|upper:ascii}"_el));
el::io::printLine(replacer.replaceOrThrow("Summary: ${var:description|safe:length=18}"_el));
el::io::printLine(replacer.replaceOrThrow("Status: ${var:status|default:pending}"_el));
}
/// Escape a value and choose text based on its contents.
void chooseFieldNoteText() {
auto replacer = el::placeholder::Replacer{};
replacer.setVariableSource(
{{{"description"_el, u8"<krater>Askja</krater>"_el}, {"state"_el, "reviewed"_el}, {"sample"_el, "A-104"_el}}});
replacer.addTextFilters();
el::io::printLine(replacer.replaceOrThrow("HTML: ${var:description|escape:format=html,amount=required}"_el));
el::io::printLine(
replacer.replaceOrThrow("Audience: ${var:state|if:contains=review,then=internal,else=public}"_el));
el::io::printLine(replacer.replaceOrThrow("Sample: ${var:sample|error_if:empty}"_el));
}
/// Report an empty value during strict replacement.
void requireFieldNoteValue() {
auto replacer = el::placeholder::Replacer{};
replacer.setVariableSource({{{"sample"_el, ""_el}}});
replacer.addTextFilters();
try {
[[maybe_unused]] const auto result = replacer.replaceOrThrow("Sample: ${var:sample|required}"_el);
} catch (const el::placeholder::ReplacerError &error) {
el::io::printLine("Required sample failed: "_el, error.reason());
}
}
}