-1

I need a function which will accept test arguments and return a formatted response

format('Hello, {name}!', {name: 'world'}); // Hello, world!

format('Hello, {user.name.first} {user.name.last}!', {user: {name: {first: 'John', last: 'Smith'}}}); // Hello, John Smith!

format('Hello, {user.name.first.f1} {user.name.last}!', {user: {name: {first: {f1:'John'}, last: 'Smith'}}}); // Hello, John Smith!

I did this. But is it a good approach ? Basic idea was to convert the string to template. Is there any better approach ?

var format = (str, obj) => {

  str = str.replaceAll('{', '${obj.');
  let newStr = eval('`' + str + '`');
    
  console.log(newStr);
}
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37

2 Answers2

0

I did this. But is it a good approach ?

I can not really tell, but ... Consider this ...

var format = (str, obj) => {

  str = str.replaceAll('{', '${obj.');
  console.log(str);

  let newStr = eval('`' + str + '`');
  console.log(newStr);
}
format('{alert("Think before \\"eval\\"uating!")}', window);
.as-console-wrapper { min-height: 100%!important; top: 0; }

Read on SO about why eval has limited use cases ... why is eval evil

Edit

For a custom template-string evaluation / interpolation I would stick to the syntax of JavaScript Template Literals of cause without enclosing the template string by backticks.

For the substitution one needs a regex which targets both the valid substitute template syntax ${ foo.bar } and a valid object path syntax. Thus variable/property names can start with $, _ and upper/lowercase latin letters only, whereas within a variable name the numbers from 0 to 9 are allowed. Thus a regex which does capture such a valid object path from a valid template syntax looks like this ...

/\$\{\s*(?<path>[a-zA-Z_$]\w*(?:\.[a-zA-Z_$]\w*)*)\s*\}/g

Value interpolation then is not that complicated anymore. One just does split an object path like 'foo.bar.baz' into an array of property names like ['foo', 'bar', 'baz']. The final value can be evaluated via a simple reduce task which programmatically walks down the property chain of the provided object/type ...

function evaluateCustomTemplate(template, type) {
  function evaluateValue(value, key) {
    return value && value[key];
  }
  function interpolateValue(match, path) {
    return path
      .split(/\./)
      .reduce(evaluateValue, type);
  }
  const regXValidObjectPath =
    // [https://regex101.com/r/OtNiAB/1/]
    (/\$\{\s*(?<path>[a-zA-Z_$]\w*(?:\.[a-zA-Z_$]\w*)*)\s*\}/g);

  return template.replace(regXValidObjectPath, interpolateValue); 
}

console.log(
  evaluateCustomTemplate(
    'Hi, ${ alert("Think before \\"eval\\"uating!") }!',
    window
  )
);

console.log(
  evaluateCustomTemplate(
    'Hello, ${ name }!',
    { name: 'world' }
  )
);
console.log(
  evaluateCustomTemplate(
    'Hello, ${ user.name.first } ${ user.name.last }!',
    { user: { name: { first: 'John', last: 'Smith' } } }
  )
);
console.log(
  evaluateCustomTemplate(
    'Hello, ${ user.name.first.f1 } ${ user.name.last }!',
    { user: { name: { first: { f1:'John' }, last: 'Smith' } } }
  )
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

The above approach based on a less rigid object-path regex like ... /\$\{([^}]+)\}/g ... then turns into the one beneath ...

function evaluateCustomTemplate(template, type) {
  function interpolateValue(match, path) {
    return path
      .trim() // minimum whitespace sanitation.
      .split(/\./)
      .reduce((value, key) => value && value[key], type);
  }
  // [https://regex101.com/r/OtNiAB/2/]
  const regXDirtyObjectPath = (/\$\{([^}]+)\}/g);

  return template.replace(regXDirtyObjectPath, interpolateValue); 
}

console.log(
  evaluateCustomTemplate(
    'Hi, ${ alert("Think before \\"eval\\"uating!") }!',
    window
  )
);

console.log(
  evaluateCustomTemplate(
    'Hello, ${ name }!',
    { name: 'world' }
  )
);
console.log(
  evaluateCustomTemplate(
    'Hello, ${ user.name.first } ${ user.name.last }!',
    { user: { name: { first: 'John', last: 'Smith' } } }
  )
);
console.log(
  evaluateCustomTemplate(
    'Hello, ${ user.name.first.f1 } ${ user.name.last }!',
    { user: { name: { first: { f1:'John' }, last: 'Smith' } } }
  )
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
0

The replaceAll method accepts a function as a second parameter, the parameter is the matched substring. You can use regex to match {abc.def.ghi} and with the function return the value at this path.

The accessByPath function is based on this SO question.

const accessByPath = (obj, path) => {
    const parts = path.split('.')
    // TODO: Properly handle invalid path
    for (let i=0; i < parts.length; i++){
        obj = obj[parts[i]];
    };
    return obj;
};

const format = (str, obj) => {
  return str.replaceAll(/{[^}]+}/g, substring => accessByPath(obj, substring.slice(1, -1)))
}

console.log(format('Hello, {name}!', {name: 'world'}))
console.log(format('Hello, {user.name.first} {user.name.last}!', {user: {name: {first: 'John', last: 'Smith'}}}))
console.log(format('Hello, {user.name.first.f1} {user.name.last}!', {user: {name: {first: {f1:'John'}, last: 'Smith'}}}))

As you asked whether yours is a good approach: Using eval in many cases introduces security risks (e.g. in your case if a user somehow can control the str value, they can execute any javascript on your page).

A_A
  • 1,832
  • 2
  • 11
  • 17