web-dev-qa-db-fra.com

Convertir une chaîne en une chaîne de modèle

Est-il possible de créer une chaîne de modèle comme une chaîne habituelle

let a="b:${b}";

puis convertissez-le en chaîne de modèle

let b=10;
console.log(a.template());//b:10

sans eval, new Function et d'autres moyens de génération de code dynamique?

101
KOLANICH

Comme votre chaîne de modèle doit avoir une référence dynamique à la variable b (au moment de l'exécution), la réponse est: NO, impossible de se passer de la génération de code dynamique.

Mais avec eval c'est assez simple:

let tpl = eval('`'+a+'`');
57
alexpods

Dans mon projet, j'ai créé quelque chose comme ça avec ES6:

String.prototype.interpolate = function(params) {
  const names = Object.keys(params);
  const vals = Object.values(params);
  return new Function(...names, `return \`${this}\`;`)(...vals);
}

const template = 'Example text: ${text}';
const result = template.interpolate({
  text: 'Foo Boo'
});
console.log(result);

UPDATE J'ai supprimé la dépendance à lodash, ES6 dispose de méthodes équivalentes pour obtenir les clés et les valeurs.

29
Mateusz Moska

Qu'est-ce que vous demandez ici:

//non working code quoted from the question
let b=10;
console.log(a.template());//b:10

est exactement équivalent (en termes de puissance et de sécurité) à eval: possibilité de prendre une chaîne contenant du code et de l'exécuter; et également la possibilité pour le code exécuté de voir les variables locales dans l'environnement de l'appelant.

Dans JS, il n’existe aucun moyen pour une fonction de voir les variables locales dans son appelant, à moins que cette fonction soit eval(). Même Function() ne peut pas le faire.


Lorsque vous entendez quelque chose appelé "modèle de chaîne" entrant dans JavaScript, il est naturel de supposer qu'il s'agit d'une bibliothèque de modèles intégrée, telle que Moustache. Ce n'est pas. Il s’agit principalement de interpolation de chaînes et de chaînes multilignes pour JS. Je pense que cela va être une idée fausse commune pendant un certain temps, cependant. :(

28
Jason Orendorff

Non, il n'y a pas moyen de faire cela sans génération de code dynamique. 

Cependant, j'ai créé une fonction qui transformera une chaîne régulière en une fonction pouvant être fournie avec une carte de valeurs, en utilisant des chaînes de modèle en interne.

Générer un modèle de chaîne de caractères Gist

/**
 * Produces a function which uses template strings to do simple interpolation from objects.
 * 
 * Usage:
 *    var makeMeKing = generateTemplateString('${name} is now the king of ${country}!');
 * 
 *    console.log(makeMeKing({ name: 'Bryan', country: 'Scotland'}));
 *    // Logs 'Bryan is now the king of Scotland!'
 */
var generateTemplateString = (function(){
    var cache = {};

    function generateTemplate(template){
        var fn = cache[template];

        if (!fn){
            // Replace ${expressions} (etc) with ${map.expressions}.

            var sanitized = template
                .replace(/\$\{([\s]*[^;\s\{]+[\s]*)\}/g, function(_, match){
                    return `\$\{map.${match.trim()}\}`;
                    })
                // Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string.
                .replace(/(\$\{(?!map\.)[^}]+\})/g, '');

            fn = Function('map', `return \`${sanitized}\``);
        }

        return fn;
    }

    return generateTemplate;
})();

Usage:

var kingMaker = generateTemplateString('${name} is king!');

console.log(kingMaker({name: 'Bryan'}));
// Logs 'Bryan is king!' to the console.

J'espère que ça aide quelqu'un. Si vous rencontrez un problème avec le code, merci de mettre à jour le Gist.

24
Bryan Rayner

TLDR: https://jsfiddle.net/w3jx07vt/

Tout le monde semble s'inquiéter de l'accès aux variables, pourquoi ne pas simplement les transmettre? Je suis sûr que ce ne sera pas trop difficile d’obtenir le contexte variable dans l’appelant et de le transmettre. Utilisez cette https://stackoverflow.com/a/6394168/6563504 pour obtenir les accessoires d'obj. Je ne peux pas tester pour vous maintenant, mais cela devrait fonctionner.

function renderString(str,obj){
    return str.replace(/\$\{(.+?)\}/g,(match,p1)=>{return index(obj,p1)})
}

Testé. Voici le code complet.

function index(obj,is,value) {
    if (typeof is == 'string')
        is=is.split('.');
    if (is.length==1 && value!==undefined)
        return obj[is[0]] = value;
    else if (is.length==0)
        return obj;
    else
        return index(obj[is[0]],is.slice(1), value);
}

function renderString(str,obj){
    return str.replace(/\$\{.+?\}/g,(match)=>{return index(obj,match)})
}

renderString('abc${a}asdas',{a:23,b:44}) //abc23asdas
renderString('abc${a.c}asdas',{a:{c:22,d:55},b:44}) //abc22asdas
8
M3D

Le problème ici est d'avoir une fonction qui a accès aux variables de l'appelant. C’est la raison pour laquelle nous voyons que la variable directe eval est utilisée pour le traitement des modèles. Une solution possible serait de générer une fonction prenant des paramètres formels nommés par les propriétés d'un dictionnaire et l'appelant avec les valeurs correspondantes dans le même ordre. Une autre solution serait d’avoir quelque chose de simple comme ceci:

var name = "John Smith";
var message = "Hello, my name is ${name}";
console.log(new Function('return `' + message + '`;')());

Et pour ceux qui utilisent le compilateur Babel, nous devons créer une fermeture qui garde en mémoire l'environnement dans lequel il a été créé:

console.log(new Function('name', 'return `' + message + '`;')(name));
6
didinko

J'ai eu besoin de cette méthode avec le support d'Internet Explorer. Il s'est avéré que les ticks arrière ne sont même pas supportés par IE11. Également; utiliser eval ou son équivalent Function ne se sent pas bien.

Pour celui qui remarque; J'utilise aussi des backticks, mais ceux-ci sont supprimés par des compilateurs comme Babel. Les méthodes suggérées par d’autres dépendent de leur exécution. Comme dit précédemment; c'est un problème dans IE11 et inférieur.

Alors voici ce que je suis venu avec:

function get(path, obj, fb = `$\{${path}}`) {
  return path.split('.').reduce((res, key) => res[key] || fb, obj);
}

function parseTpl(template, map, fallback) {
  return template.replace(/\$\{.+?}/g, (match) => {
    const path = match.substr(2, match.length - 3).trim();
    return get(path, map, fallback);
  });
}

Exemple de sortie:

const data = { person: { name: 'John', age: 18 } };

parseTpl('Hi ${person.name} (${person.age})', data);
// output: Hi John (18)

parseTpl('Hello ${person.name} from ${person.city}', data);
// output: Hello John from ${person.city}

parseTpl('Hello ${person.name} from ${person.city}', data, '-');
// output: Hello John from -
4
s.meijer

Semblable à la réponse de Daniel (et à mon ami Gist ) mais plus lisible:

const regex = /\${[^{]+}/g;

export default function interpolate(template, variables, fallback) {
    return template.replace(regex, (match) => {
        const path = match.slice(2, -1).trim();
        return getObjPath(path, variables, fallback);
    });
}

//get the specified property or nested property of an object
function getObjPath(path, obj, fallback = '') {
    return path.split('.').reduce((res, key) => res[key] || fallback, obj);
}

Remarque: ceci améliore légèrement l'original de mon joueur, car il ne correspond pas à des éléments tels que ${foo{bar} (la regex n'autorise que les caractères non bouclés entre ${ et }).


UPDATE: On m'a demandé un exemple en utilisant ceci, alors voilà:

const replacements = {
    name: 'Bob',
    age: 37
}

interpolate('My name is ${name}, and I am ${age}.', replacements)
4
Matt Browne

Vous pouvez utiliser le prototype de chaîne, par exemple

String.prototype.toTemplate=function(){
    return eval('`'+this+'`');
}
//...
var a="b:${b}";
var b=10;
console.log(a.toTemplate());//b:10

Mais la réponse à la question initiale est impossible.

4
sarkiroka

Je ne peux actuellement pas commenter les réponses existantes, je ne peux donc pas commenter directement l'excellente réponse de Bryan Raynor. Ainsi, cette réponse va mettre à jour sa réponse avec une légère correction.

En bref, sa fonction ne parvient pas à mettre en cache la fonction créée. Elle sera donc toujours recréée, qu’elle ait été vue ou non auparavant. Voici le code corrigé:

    /**
     * Produces a function which uses template strings to do simple interpolation from objects.
     * 
     * Usage:
     *    var makeMeKing = generateTemplateString('${name} is now the king of ${country}!');
     * 
     *    console.log(makeMeKing({ name: 'Bryan', country: 'Scotland'}));
     *    // Logs 'Bryan is now the king of Scotland!'
     */
    var generateTemplateString = (function(){
        var cache = {};

        function generateTemplate(template){
            var fn = cache[template];

            if (!fn){
                // Replace ${expressions} (etc) with ${map.expressions}.

                var sanitized = template
                    .replace(/\$\{([\s]*[^;\s\{]+[\s]*)\}/g, function(_, match){
                        return `\$\{map.${match.trim()}\}`;
                    })
                    // Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string.
                    .replace(/(\$\{(?!map\.)[^}]+\})/g, '');

                fn = cache[template] = Function('map', `return \`${sanitized}\``);
            }

            return fn;
        };

        return generateTemplate;
    })();
3
user2501097

Toujours dynamique, mais qui semble plus contrôlé que l’utilisation d’un eval nu:

const vm = require('vm')
const moment = require('moment')


let template = '### ${context.hours_worked[0].value} \n Hours worked \n #### ${Math.abs(context.hours_worked_avg_diff[0].value)}% ${fns.gt0(context.hours_worked_avg_diff[0].value, "more", "less")} than usual on ${fns.getDOW(new Date())}'
let context = {
  hours_worked:[{value:10}],
  hours_worked_avg_diff:[{value:10}],

}


function getDOW(now) {
  return moment(now).locale('es').format('dddd')
}

function gt0(_in, tVal, fVal) {
  return _in >0 ? tVal: fVal
}



function templateIt(context, template) {
  const script = new vm.Script('`'+template+'`')
  return script.runInNewContext({context, fns:{getDOW, gt0 }})
}

console.log(templateIt(context, template))

https://repl.it/IdVt/3

2
Robert Moskal

@Mateusz Moska, la solution fonctionne très bien, mais lorsque je l'utilisais dans React Native (mode compilation), une erreur se présentait: Caractère non valide '' ', bien que cela fonctionne lorsque je l'exécutais en mode débogage.

J'ai donc écrit ma propre solution en utilisant regex.

String.prototype.interpolate = function(params) {
  let template = this
  for (let key in params) {
    template = template.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), params[key])
  }
  return template
}

const template = 'Example text: ${text}',
  result = template.interpolate({
    text: 'Foo Boo'
  })

console.log(result)

Démo:https://es6console.com/j31pqx1p/

NOTE: Étant donné que je ne connais pas la cause première d'un problème, j'ai généré un ticket dans le référentiel réact-native, https://github.com/facebook/react-native/issues/14107 , de sorte qu’une fois qu’ils soient capables de réparer/me guider à propos de la même chose :) 

2
Mohit Pandey

J'ai aimé à la réponse de mon mari et écrit ma propre version basée sur son

function parseTemplate(template, map, fallback) {
    return template.replace(/\$\{[^}]+\}/g, (match) => 
        match
            .slice(2, -1)
            .trim()
            .split(".")
            .reduce(
                (searchObject, key) => searchObject[key] || fallback || match,
                map
            )
    );
}
2
Daniel

Puisque nous réinventons la roue sur quelque chose qui constituerait une fonctionnalité intéressante en javascript.

J'utilise eval(), qui n'est pas sécurisé, mais javascript n'est pas sécurisé. J'admets volontiers que je ne suis pas excellent avec javascript, mais j'avais un besoin et j'avais besoin d'une réponse, alors j'en ai créé une.

J'ai choisi de styliser mes variables avec un @ plutôt qu'un $, en particulier parce que je veux utiliser la fonctionnalité multiligne de littéraux sans jusqu'à ce qu'elle soit prête. Donc, la syntaxe de la variable est @{OptionalObject.OptionalObjectN.VARIABLE_NAME}

Je ne suis pas un expert du javascript, je prendrai volontiers des conseils pour améliorer les choses, mais ...

var prsLiteral, prsRegex = /\@\{(.*?)(?!\@\{)\}/g
for(i = 0; i < myResultSet.length; i++) {
    prsLiteral = rt.replace(prsRegex,function (match,varname) {
        return eval(varname + "[" + i + "]");
        // you could instead use return eval(varname) if you're not looping.
    })
    console.log(prsLiteral);
}

Une implémentation très simple suit

myResultSet = {totalrecords: 2,
Name: ["Bob", "Stephanie"],
Age: [37,22]};

rt = `My name is @{myResultSet.Name}, and I am @{myResultSet.Age}.`

var prsLiteral, prsRegex = /\@\{(.*?)(?!\@\{)\}/g
for(i = 0; i < myResultSet.totalrecords; i++) {
    prsLiteral = rt.replace(prsRegex,function (match,varname) {
        return eval(varname + "[" + i + "]");
        // you could instead use return eval(varname) if you're not looping.
    })
    console.log(prsLiteral);
}

Dans mon implémentation actuelle, j'ai choisi d'utiliser @{{variable}}. Un autre ensemble d'accolades. Absolument improbable de rencontrer cela de façon inattendue. La regex pour cela ressemblerait à /\@\{\{(.*?)(?!\@\{\{)\}\}/g

Pour rendre cela plus facile à lire

\@\{\{    # opening sequence, @{{ literally.
(.*?)     # capturing the variable name
          # ^ captures only until it reaches the closing sequence
(?!       # negative lookahead, making sure the following
          # ^ pattern is not found ahead of the current character
  \@\{\{  # same as opening sequence, if you change that, change this
)
\}\}      # closing sequence.

Si vous n’êtes pas habitué aux expressions rationnelles, une règle plutôt sûre consiste à échapper à tous les caractères non alphanumériques et à ne jamais jamais échapper inutilement aux lettres car de nombreuses lettres échappées ont une signification particulière pour pratiquement tous les goûts de regex.

0
Regular Joe

Vous devriez essayer ce petit module JS, par Andrea Giammarchi, de github: https://github.com/WebReflection/backtick-template

/*! (C) 2017 Andrea Giammarchi - MIT Style License */
function template(fn, $str, $object) {'use strict';
  var
    stringify = JSON.stringify,
    hasTransformer = typeof fn === 'function',
    str = hasTransformer ? $str : fn,
    object = hasTransformer ? $object : $str,
    i = 0, length = str.length,
    strings = i < length ? [] : ['""'],
    values = hasTransformer ? [] : strings,
    open, close, counter
  ;
  while (i < length) {
    open = str.indexOf('${', i);
    if (-1 < open) {
      strings.Push(stringify(str.slice(i, open)));
      open += 2;
      close = open;
      counter = 1;
      while (close < length) {
        switch (str.charAt(close++)) {
          case '}': counter -= 1; break;
          case '{': counter += 1; break;
        }
        if (counter < 1) {
          values.Push('(' + str.slice(open, close - 1) + ')');
          break;
        }
      }
      i = close;
    } else {
      strings.Push(stringify(str.slice(i)));
      i = length;
    }
  }
  if (hasTransformer) {
    str = 'function' + (Math.random() * 1e5 | 0);
    if (strings.length === values.length) strings.Push('""');
    strings = [
      str,
      'with(this)return ' + str + '([' + strings + ']' + (
        values.length ? (',' + values.join(',')) : ''
      ) + ')'
    ];
  } else {
    strings = ['with(this)return ' + strings.join('+')];
  }
  return Function.apply(null, strings).apply(
    object,
    hasTransformer ? [fn] : []
  );
}

template.asMethod = function (fn, object) {'use strict';
  return typeof fn === 'function' ?
    template(fn, this, object) :
    template(this, fn);
};

Démo (tous les tests suivants renvoient vrai):

const info = 'template';
// just string
`some ${info}` === template('some ${info}', {info});

// passing through a transformer
transform `some ${info}` === template(transform, 'some ${info}', {info});

// using it as String method
String.prototype.template = template.asMethod;

`some ${info}` === 'some ${info}'.template({info});

transform `some ${info}` === 'some ${info}'.template(transform, {info});
0
colxi