skip to Main Content

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


  1. You can use

    (?<=(?<!{)(?:{{)*){d+}(?!(?:}})*}(?!}))
    

    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:

    function format(template, ...values) {
      const regex = /(?<=(?<!{)(?:{{)*){d+}(?!(?:}})*}(?!}))/g;
      return template.replace(regex, ([, index]) => {
        const valueIndex = parseInt(index, 10);
        if (valueIndex >= values.length) throw new Error("Not enough arguments")
        return String(values[valueIndex]).replaceAll('{{','{').replaceAll('}}','}');
    
      });
    }
    
    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`
    ]);
    
    try {
      format("{0} {1}", 42); // throw error because not enough argument are passed
    } catch (e) {
      console.error(e);
    }

    NOTE: You need to "prep" the values passed to the functions so that the braces are escaped, i.e. just use value.replaceAll('{','{{').replaceAll('}','}}').

    Login or Signup to reply.
  2. I would suggest replacing the double braces in the same replace call:

    function format(template, ...values) {
      const regex = /({{|}})|{(d+)}(?=(}})*(?!}))/g;
      return template.replace(regex, (_, braces, index) => {
        if (braces) return braces[0];
        if (+index >= values.length) throw new Error("Not enough arguments")
        return values[index];
      });
    }
    
    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`
    ]);
    
    try {
      format("{0} {1}", 42); // throw error because not enough argument are passed
    } catch (e) {
      console.log(e.message);
    }

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search