loop - template string javascript
Convertir una cadena en una cadena de plantilla (16)
¿Es posible crear una cadena de plantilla como una cadena habitual?
let a="b:${b}";
y luego convertirlo en una cadena de plantilla
let b=10;
console.log(a.template());//b:10
sin
eval
,
new Function
y otros medios de generación dinámica de código?
@Mateusz Moska, la solución funciona muy bien, pero cuando la usé en React Native (modo de compilación), arroja un error: Carácter no válido '''' '' , aunque funciona cuando lo ejecuto en modo de depuración.
Así que escribí mi propia solución usando 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)
Demostración: https://es6console.com/j31pqx1p/
NOTA: Como no conozco la causa raíz de un problema, planteé un ticket en el repositorio react-native, https://github.com/facebook/react-native/issues/14107 , para que una vez que puedan arreglar / guiarme por lo mismo :)
Actualmente no puedo comentar sobre las respuestas existentes, así que no puedo comentar directamente sobre la excelente respuesta de Bryan Raynor. Por lo tanto, esta respuesta va a actualizar su respuesta con una ligera corrección.
En resumen, su función no puede en realidad almacenar en caché la función creada, por lo que siempre se recreará, independientemente de si se ha visto la plantilla antes. Aquí está el código corregido:
/**
* 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;
})();
Como su cadena de plantilla debe obtener referencia a la variable
b
dinámicamente (en tiempo de ejecución), la respuesta es:
NO, es imposible hacerlo sin la generación dinámica de código.
Pero con
eval
es bastante simple:
let tpl = eval(''`''+a+''`'');
Deberías probar este pequeño módulo JS, de 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);
};
Demostración (todas las siguientes pruebas son verdaderas):
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});
El problema aquí es tener una función que tenga acceso a las variables de su llamador.
Esta es la razón por la que vemos que se usa
eval
directa para el procesamiento de plantillas.
Una posible solución sería generar una función tomando parámetros formales nombrados por las propiedades de un diccionario y llamándolo con los valores correspondientes en el mismo orden.
Una forma alternativa sería tener algo simple como esto:
var name = "John Smith";
var message = "Hello, my name is ${name}";
console.log(new Function(''return `'' + message + ''`;'')());
Y para cualquiera que use el compilador de Babel, necesitamos crear un cierre que recuerde el entorno en el que se creó:
console.log(new Function(''name'', ''return `'' + message + ''`;'')(name));
En mi proyecto, he creado algo como esto con 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);
ACTUALIZACIÓN He eliminado la dependencia lodash, ES6 tiene métodos equivalentes para obtener claves y valores.
Esta solución funciona sin ES6:
function render(template, opts) {
return new Function(
''return new Function ('' + Object.keys(opts).reduce((args, arg) => args += ''/''' + arg + ''/','', '''') + ''/'return `'' + template.replace(/(^|[^//])''/g, ''$1///''') + ''`;/''' +
'').apply(null, '' + JSON.stringify(Object.keys(opts).reduce((vals, key) => vals.push(opts[key]) && vals, [])) + '');''
)();
}
render("hello ${ name }", {name:''mo''}); // "hello mo"
Nota: el constructor de
Function
siempre se crea en el ámbito global, lo que podría provocar que la plantilla sobrescriba las variables globales, por ejemplo,
render("hello ${ someGlobalVar = ''some new value'' }", {name:''mo''});
Lo que estás pidiendo aquí:
//non working code quoted from the question let b=10; console.log(a.template());//b:10
es exactamente equivalente (en términos de potencia y, por ejemplo, seguridad) a
eval
: la capacidad de tomar una cadena que contiene código y ejecutar ese código;
y también la capacidad del código ejecutado para ver variables locales en el entorno del llamante.
No hay forma en JS para que una función vea variables locales en su llamador, a menos que esa función sea
eval()
.
Incluso
Function()
no puede hacerlo.
Cuando escuche que hay algo llamado "cadenas de plantillas" en JavaScript, es natural suponer que es una biblioteca de plantillas incorporada, como Moustache. No lo es Es principalmente solo interpolación de cadenas y cadenas multilíneas para JS. Sin embargo, creo que esto será un error común durante un tiempo. :(
Me gustó la respuesta de s.meijer y escribí mi propia versión basada en la suya:
function parseTemplate(template, map, fallback) {
return template.replace(//$/{[^}]+/}/g, (match) =>
match
.slice(2, -1)
.trim()
.split(".")
.reduce(
(searchObject, key) => searchObject[key] || fallback || match,
map
)
);
}
No, no hay forma de hacerlo sin la generación dinámica de código.
Sin embargo, he creado una función que convertirá una cadena normal en una función que se puede proporcionar con un mapa de valores, utilizando cadenas de plantilla internamente.
Generar plantilla de cadena 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;
})();
Uso:
var kingMaker = generateTemplateString(''${name} is king!'');
console.log(kingMaker({name: ''Bryan''}));
// Logs ''Bryan is king!'' to the console.
Espero que esto ayude a alguien. Si encuentra un problema con el código, sea tan amable de actualizar el Gist.
Puede usar el prototipo de cadena, por ejemplo
String.prototype.toTemplate=function(){
return eval(''`''+this+''`'');
}
//...
var a="b:${b}";
var b=10;
console.log(a.toTemplate());//b:10
Pero la respuesta de la pregunta original es de ninguna manera.
Requerí este método con soporte para Internet Explorer.
Resultó que los ticks posteriores no son compatibles ni siquiera con IE11.
También;
usando
eval
o su equivalente La
Function
no se siente bien.
Para el que nota; También uso backticks, pero estos son eliminados por compiladores como babel. Los métodos sugeridos por otros dependen de ellos en el tiempo de ejecución. Como se dijo antes; Este es un problema en IE11 y versiones inferiores.
Entonces esto es lo que se me ocurrió:
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);
});
}
Salida de ejemplo:
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 -
Similar a la respuesta de Daniel (y la gist.github.com/smeijer/6580740a0ff468960a5257108af1384e de mi mejer) pero más legible:
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);
}
Nota: Esto mejora ligeramente el original de s.meijer, ya que no coincidirá con cosas como
${foo{bar}
(la expresión regular solo permite caracteres de llaves no rizadas dentro de
${
y
}
).
ACTUALIZACIÓN: me pidieron un ejemplo usando esto, así que aquí tienes:
const replacements = {
name: ''Bob'',
age: 37
}
interpolate(''My name is ${name}, and I am ${age}.'', replacements)
TLDR: https://jsfiddle.net/w3jx07vt/
Todos parecen estar preocupados por acceder a las variables, ¿por qué no simplemente pasarlas? Estoy seguro de que no será demasiado difícil obtener el contexto variable en la persona que llama y transmitirlo. Use este https://.com/a/6394168/6563504 para obtener los accesorios de obj. No puedo hacerte una prueba en este momento, pero esto debería funcionar.
function renderString(str,obj){
return str.replace(//$/{(.+?)/}/g,(match,p1)=>{return index(obj,p1)})
}
Probado Aquí está el código completo.
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
Todavía dinámico pero parece más controlado que simplemente usando una evaluación desnuda:
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))
Ya que estamos reinventando la rueda en algo que sería una característica encantadora en javascript.
Yo uso
eval()
, que no es seguro, pero JavaScript no es seguro.
Admito que no soy excelente con javascript, pero tenía una necesidad y necesitaba una respuesta, así que hice una.
Elegí estilizar mis variables con un
@
lugar de un
$
, particularmente porque quiero usar la función multilínea de literales
sin
evaluar hasta que esté lista.
Entonces, la sintaxis variable es
@{OptionalObject.OptionalObjectN.VARIABLE_NAME}
No soy un experto en JavaScript, por lo que con gusto tomaría consejos sobre mejoras pero ...
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);
}
Sigue una implementación muy simple
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);
}
En mi implementación real, elijo usar
@{{variable}}
.
Un juego más de llaves.
Absurdamente improbable encontrar eso inesperadamente.
La expresión regular para eso se vería como
//@/{/{(.*?)(?!/@/{/{)/}/}/g
Para que sea más fácil de leer
/@/{/{ # 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 no tiene experiencia con expresiones regulares, una regla bastante segura es escapar de todos los caracteres no alfanuméricos, y nunca escapar innecesariamente de las letras, ya que muchas letras tienen un significado especial para prácticamente todos los sabores de expresiones regulares.