I want to create a simple text templating that allow defining placeholders using {0}
, similar to what .Net does using string.format method.
Basically I want this:
format("{0}", 42), // output `42`
format("{0} {1}", 42, "bar"), // output `42 bar`
format("{1} {1}", 42, "bar"), // output `bar bar` ({0} ignored)
format("{{0", 42), // output `{0` (`{{` is an escaped `{`)
format("{{{0}", 42), // output `{42` : an escaped brace and the formatted value
format("Mix {{0}} and {0}", 42), // outputs `Mix {0} and 42`
format("Invalid closing brace }"), // should fail, since the closing brace does close an opening one
format("Invalid placeholder {z}"), // should fail, not an integer
format("{0}", "With { in value"), // output `With { in value`, inner { should be broke the format
I’m trying to play with regex and backtracking to deal with the escaped braces.
function format(template: string, ...values: unknown[]): string {
const regex = /(?!({{)+){(d+)}(?<!(}}))/gm;
return template.replace(regex, ([, index]) => {
const valueIndex = parseInt(index, 10);
if (valueIndex >= values.length) throw new Error("Not enough arguments")
return String(values[valueIndex]);
});
}
console.log([
format("{0}", 42), // output `42`
format("{0} {1}", 42, "bar"), // output `42 bar`
format("{1} {1}", 42, "bar"), // output `bar bar` ({0} ignored)
format("{{0", 42), // output `{0` (`{{` is an escaped `{`)
format("{{{0}", 42), // output `{42` : an escaped brace and the formatted value
format("Mix {{0}} and {0}", 42), // outputs `Mix {0} and 42`
format("Invalid closing brace }"), // should fail, since the closing brace does not close an opening one
format("Invalid placeholder {z}"), // should fail, not an integer
format("{0}", "With { in value"), // output `With { in value`, inner { should be broke the format
]);
try {
format("{0} {1}", 42); // throw error because not enough argument are passed
} catch (e) {
console.log(e.message);
}
However, I’m struggling to properly replaced the escaped braces by a single brace
How to fix it ?
2
Answers
You can use
See the regex demo.
Details:
(?<=(?<!{)(?:{{)*){
– a{
char that is not escaped (there must not be a{
followed with zero or more double{
chars)d+
}(?!(?:}})*}(?!}))
– a}
char that is not escaped (there must not be a}
preceded with zero or more double}
chars)The
.replaceAll('{{','{').replaceAll('}}','}')
part finishes the transformation.See the JS demo:
NOTE: You need to "prep" the values passed to the functions so that the braces are escaped, i.e. just use
value.replaceAll('{','{{').replaceAll('}','}}')
.I would suggest replacing the double braces in the same
replace
call:The lookahead assertion asserts that the number of adjacent closing braces (after the one after the digits) is even, as otherwise the first closing brace should be interpreted as an escaping one.
There is no need to have this assertion for the opening braces, as they are matched in pairs from left to right. There is no risk that in backtracking these pairs get chunked up in the wrong way.