Можно ли заменить литералы шаблона ES6 во время выполнения (или использовать повторно)?


129

tl; dr: Можно ли сделать литерал многоразового шаблона?

Я пытался использовать литералы шаблонов, но, кажется, просто не понимаю, и теперь я расстраиваюсь. Я имею в виду, я думаю, что понимаю это, но «это» не должно быть таким, как оно работает или как оно должно быть получено. Должно получиться иначе.

Все примеры, которые я вижу (даже шаблоны с тегами), требуют, чтобы «подстановки» выполнялись во время объявления, а не во время выполнения, что мне кажется совершенно бесполезным для шаблона. Может быть, я сумасшедший, но для меня «шаблон» - это документ, который содержит токены, которые заменяются, когда вы его используете, а не когда вы его создаете, иначе это просто документ (то есть строка). Шаблон хранится с токенами как токенами, и эти токены оцениваются, когда вы ... оцениваете его.

Все приводят ужасный пример, похожий на:

var a = 'asd';
return `Worthless ${a}!`

Это хорошо, но если я уже знаю a, я бы просто return 'Worthless asd'или return 'Worthless '+a. В чем смысл? Шутки в сторону. Ладно, дело в лени; меньше плюсов, больше читабельности. Отлично. Но это не шаблон! Не ИМХО. И MHO - это все, что имеет значение! Проблема, IMHO, в том, что шаблон оценивается, когда он объявлен, поэтому, если вы это сделаете, IMHO:

var tpl = `My ${expletive} template`;
function go() { return tpl; }
go(); // SPACE-TIME ENDS!

Поскольку expletiveне объявлен, он выводит что-то вроде My undefined template. Супер. На самом деле, по крайней мере, в Chrome я даже не могу объявить шаблон; он выдает ошибку, потому что expletiveне определен. Что мне нужно, так это иметь возможность выполнить замену после объявления шаблона:

var tpl = `My ${expletive} template`;
function go() { return tpl; }
var expletive = 'great';
go(); // My great template

Однако я не понимаю, как это возможно, поскольку на самом деле это не шаблоны. Даже когда вы говорите, что я должен использовать теги, нет, они не работают:

> explete = function(a,b) { console.log(a); console.log(b); }
< function (a,b) { console.log(a); console.log(b); }
> var tpl = explete`My ${expletive} template`
< VM2323:2 Uncaught ReferenceError: expletive is not defined...

Все это привело меня к мысли, что литералы шаблонов ужасно неверно названы и их следует называть тем, чем они являются на самом деле: heredocs . Я думаю, что "буквальная" часть должна была меня предупредить (например, неизменяемая)?

Я что-то упускаю? Есть ли (хороший) способ сделать многократно используемый шаблон литералом?


Я даю вам многоразовые шаблонные литералы :

> function out(t) { console.log(eval(t)); }
  var template = `\`This is
  my \${expletive} reusable
  template!\``;
  out(template);
  var expletive = 'curious';
  out(template);
  var expletive = 'AMAZING';
  out(template);
< This is
  my undefined reusable
  template!
  This is
  my curious reusable
  template!
  This is
  my AMAZING reusable
  template!

А вот и наивная «помощница» ...

function t(t) { return '`'+t.replace('{','${')+'`'; }
var template = t(`This is
my {expletive} reusable
template!`);

... чтобы было "лучше".

Я склонен называть их шаблонными гутералами из-за области, в которой они вызывают странные чувства.


1
Он поддерживает зачеркивание (но не в подобных комментариях). Поместите свой текст в <strike>тег.
Pointy

Литералы шаблонов ES6 в основном предназначены для старомодной интерполяции строк. Если вам нужны динамические шаблоны, используйте Handlebars и т. Д. Или решение с тегами Pointy.
joews

1
Строки шаблонов, несмотря на название, не являются шаблонами . См. Также Отложенное выполнение для шаблонных строк ES6
Берги

8
Не могли бы вы сделать свой пост менее громким? Кроме того, похоже, вы намеревались написать учебное пособие в формате вопросов и ответов . Если вы это сделали, пожалуйста, удалите часть « Я даю вам… » из своего вопроса и опубликуйте ее в качестве ответа .
Берги

Я заметил, что здесь есть много хороших ответов. Возможно, приму одного.
abalter

Ответы:


86

Чтобы эти литералы работали, как другие движки шаблонов, необходима промежуточная форма.

Лучший способ сделать это - использовать Functionконструктор.

const templateString = "Hello ${this.name}!";
const templateVars = {
    name: "world"    
}

const fillTemplate = function(templateString, templateVars){
    return new Function("return `"+templateString +"`;").call(templateVars);
}

console.log(fillTemplate(templateString, templateVars));

Как и в случае с другими шаблонизаторами, вы можете получить эту строку из других мест, например из файла.

При использовании этого метода могут возникнуть проблемы, например, с использованием тегов шаблонов, которые трудно использовать, но их можно добавить, если вы сообразительны. У вас также не может быть встроенной логики JavaScript из-за поздней интерполяции. Это тоже можно исправить, если подумать.


8
Ницца! Вы даже можете использоватьnew Function(`return \`${template}\`;`)
Рубен

И эти шаблоны могут быть составлены или «включены» с помощью аргументов путем вызова метода или передачи скомпилированного результата другого шаблона.
Квентин Энглс

Квентин, что означает «без тегов шаблона»? Спасибо!
mikemaccana

10
имейте в виду, что эта строка шаблона является своего рода «скрытой» для транспиляции (например, веб-пакет) и, следовательно, НЕ будет преобразована во что-то достаточно совместимое (например, IE11) на стороне клиента ...!
Фрэнк Нок

9
XSS-уязвимость ? Подробности в ЭТОМ JSFIDDLE
Камил Келчевски

65

Вы можете поместить строку шаблона в функцию:

function reusable(a, b) {
  return `a is ${a} and b is ${b}`;
}

То же самое можно сделать и с шаблоном с тегами:

function reusable(strings) {
  return function(... vals) {
    return strings.map(function(s, i) {
      return `${s}${vals[i] || ""}`;
    }).join("");
  };
}

var tagged = reusable`a is ${0} and b is ${1}`; // dummy "parameters"
console.log(tagged("hello", "world"));
// prints "a is hello b is world"
console.log(tagged("mars", "jupiter"));
// prints "a is mars b is jupiter"

Идея состоит в том, чтобы позволить синтаксическому анализатору шаблона отделять постоянные строки от переменной «slots», а затем возвращать функцию, которая исправляет все вместе, каждый раз на основе нового набора значений.


3
@FelixKling, что может быть; Проверю и исправлю если да. редактировать да, похоже, я упустил значительную часть примера, которая является "многоразовой" функцией :)
Pointy

@FelixKling Я не знаю, что мне делать, потому что я совершенно не могу вспомнить, о чем я думал в то время!
Pointy

1
Да, не имеет большого смысла быть tbh;) Вы всегда можете удалить его .... но reusableможно реализовать так, чтобы он возвращал функцию, и вы использовали бы ${0}и ${1}внутри литерала вместо ${a}и ${b}. Затем вы можете использовать эти значения для ссылки на аргументы функции, аналогично тому, что делает Берги в своем последнем примере: stackoverflow.com/a/22619256/218196 (или, я думаю, это в основном то же самое).
Феликс Клинг,

1
@FelixKling Хорошо, я думаю, что придумал что-то, что хотя бы отдаленно похоже на OP.
Pointy

3
Шаблоны с тегами могут быть действительно мощными, если результат на самом деле не является строкой. Например, в одном из моих проектов я использую его для интерполяции узлов AST. Например, можно expression`a + ${node}`создать узел BinaryExpression с существующим узлом AST node. Внутри мы вставляем заполнитель для генерации действительного кода, анализируем его как AST и заменяем заполнитель переданным значением.
Феликс Клинг,

45

Вероятно, самый чистый способ сделать это - использовать стрелочные функции (потому что на данный момент мы уже используем ES6)

var reusable = () => `This ${object} was created by ${creator}`;

var object = "template string", creator = "a function";
console.log (reusable()); // "This template string was created by a function"

object = "example", creator = "me";
console.log (reusable()); // "This example was created by me"

... И для тегированных шаблонных литералов:

reusable = () => myTag`The ${noun} go ${verb} and `;

var noun = "wheels on the bus", verb = "round";
var myTag = function (strings, noun, verb) {
    return strings[0] + noun + strings[1] + verb + strings[2] + verb;
};
console.log (reusable()); // "The wheels on the bus go round and round"

noun = "racecars", verb = "fast";
myTag = function (strings, noun, verb) {
    return strings[0] + noun + strings[1] + verb;
};
console.log (reusable()); // "The racecars go fast"

Это также позволяет избежать использования eval()или, Function()которые могут вызвать проблемы с компиляторами и вызвать значительное замедление.


Я думаю, что это один из лучших вариантов, потому что вы можете вводить код внутри функции, myTagчтобы что-то делать. Например, используйте параметры ввода в качестве ключа для кеширования вывода.
WOW

Думаю, это лучший ответ. Вы также можете добавить параметры функции стрелки , которые я думаю , что делает его еще чище: var reusable = (value: string) => `Value is ${value}`.
haggisandchips

13

Ответ 2019 :

Примечание . Изначально библиотека ожидала, что пользователи будут очищать строки, чтобы избежать XSS. Версия 2 библиотеки больше не требует дезинфекции пользовательских строк (что веб-разработчики должны делать в любом случае), поскольку она evalполностью этого избегает .

es6-dynamic-templateМодуль на НОМ это делает.

const fillTemplate = require('es6-dynamic-template');

В отличие от текущих ответов:

  • Он использует строки шаблона ES6, а не аналогичный формат. В обновлении версии 2 используется аналогичный формат, а не строки шаблона ES6, чтобы пользователи не могли использовать необработанные входные строки.
  • thisСтрока шаблона не нужна
  • Вы можете указать строку шаблона и переменные в одной функции
  • Это поддерживаемый, обновляемый модуль, а не копипаста из StackOverflow.

Использование простое. Используйте одинарные кавычки, так как строка шаблона будет разрешена позже!

const greeting = fillTemplate('Hi ${firstName}', {firstName: 'Joe'});

Если вы используете это с React Native, он сломается специально на Android. Среда выполнения узла Android не поддерживает динамические шаблоны, а только предварительно заполненные,
Оливер Диксон,

1
Это решение, которое я использую в своих личных проектах, и оно работает безупречно. Я вообще-то считаю, что использовать слишком много библиотек, особенно для таких небольших утилит, - плохая идея.
Оливер Диксон

1
XSS-уязвимость ? Подробности в ЭТОЙ СКРЕПКЕ
Камил Келчевски

1
@kamil Только XSS, если вы а) даете пользователям возможность создавать б) не дезинфицируете входные строки. Я добавлю предупреждение, что люди должны дезинфицировать вводимые пользователем данные.
mikemaccana

1
Это не использует удаленно литералы шаблона es6. Попробуйте, 10 * 20 = ${10 * 20}чтобы это был аналогичный формат, но это даже не литералы шаблона es6 удаленно
gman

12

Да, вы можете сделать это, проанализировав вашу строку с шаблоном как JS с помощью Function(или eval) - но это не рекомендуется и разрешает атаку XSS

Вместо этого вы можете безопасно вставлять objполя объекта в шаблон strдинамически следующим образом

let inject = (str, obj) => str.replace(/\${(.*?)}/g, (x,g)=> obj[g]);


Я использую этот метод, и он хорошо зарекомендовал себя. Хороший пример! Есть ли? после * в справке RegEx? Я не эксперт по регулярным выражениям, но предполагаю, что, поскольку * означает ноль или более (и в данном случае вам нужно «больше»), нет необходимости в жадном ограничении?
Gen1-1

@ Gen1-1 на .*?средства нежадным - если убрать , "?"то фрагмент кода даст неправильный результат
Камила Kiełczewski

Вы правы, моя ошибка. Я не использую $ в своих шаблонах, а использую RegEx: / {(\ w *)} / g, потому что у меня никогда не бывает пробелов в теге, но. *? тоже работает. Я использую:function taggedTemplate(template, data, matcher) { if (!template || !data) { return template; } matcher = matcher || /{(\w*)}/g; // {one or more alphanumeric characters with no spaces} return template.replace(matcher, function (match, key) { var value; try { value = data[key] } catch (e) { // } return value || ""; }); }
Gen1-1

@ Gen1-1 возможны ли также вложенные данные? вроде data = { a: 1, b: { c:2, d:3 } }-> b.c?
muescha

1
@muescha Вы бы изменили строку: value = data [key], чтобы использовать рекурсию и искать весь ваш объект данных и вложенные объекты, пока не найдете свойство. Примеры: codereview.stackexchange.com/questions/73714/… и mikedoesweb.com/2016/es6-depth-first-object-tree-search
Gen1-1,

9

Упрощение ответа @metamorphasi;

const fillTemplate = function(templateString, templateVars){
  var func = new Function(...Object.keys(templateVars),  "return `"+templateString +"`;")
  return func(...Object.values(templateVars));
}

// Sample
var hosting = "overview/id/d:${Id}";
var domain = {Id:1234, User:22};
var result = fillTemplate(hosting, domain);

console.log(result);


Этот код более понятен, чем основной ответ. Получил свой голос :)
ymz

Это должно позволить вам использовать переменные или внешние файлы (в NodeJS) в качестве шаблонов или динамически создавать их во время выполнения. Без использования eval.
b01

XSS-уязвимость ? Поиграйте с вредоносным кодом (переменная var hosting) ЗДЕСЬ .
Камил Келчевски

7

Если вы не хотите использовать заказанные параметры или контекст / пространства имена для ссылки на переменных в шаблоне, например ${0}, ${this.something}или ${data.something}, вы можете иметь шаблонную функцию , которая заботится о обзорного для вас.

Пример того, как вы могли бы вызвать такой шаблон:

const tempGreet = Template(() => `
  <span>Hello, ${name}!</span>
`);
tempGreet({name: 'Brian'}); // returns "<span>Hello, Brian!</span>"

Функция шаблона:

function Template(cb) {
  return function(data) {
    const dataKeys = [];
    const dataVals = [];
    for (let key in data) {
      dataKeys.push(key);
      dataVals.push(data[key]);
    }
    let func = new Function(...dataKeys, 'return (' + cb + ')();');
    return func(...dataVals);
  }
}

Причуда в этом случае заключается в том, что вам просто нужно передать функцию (в примере я использовал стрелочную функцию), которая возвращает литерал шаблона ES6. Я думаю, что это незначительный компромисс, чтобы получить такую ​​интерполяцию, которая нам нужна.

Вот он на GitHub: https://github.com/Adelphos/ES6-Reuseable-Template


3
Это хорошо, но минификация (vals, func и т. Д.) Не нужна, cb не является обратным вызовом (это полностью код синхронизации), и вы можете просто использовать Object.values()andObject.keys()
mikemaccana

3

Короткий ответ - просто используйте _.template в lodash

// Use the ES template literal delimiter as an "interpolate" delimiter.
// Disable support by replacing the "interpolate" delimiter.
var compiled = _.template('hello ${ user }!');
compiled({ 'user': 'pebbles' });
// => 'hello pebbles!'

3

Я что-то упускаю? Есть ли [хороший] способ сделать многократно используемый шаблон литералом?

Возможно, мне что- то не хватает, потому что мое решение этой проблемы кажется мне настолько очевидным, что я очень удивлен, что никто не написал это уже в таком старом вопросе.

У меня для этого есть почти однострочный:

function defer([first, ...rest]) {
  return (...values) => rest.reduce((acc, str, i) => acc + values[i] + str, first);
}

Вот и все. Когда я хочу повторно использовать шаблон и отложить разрешение замен, я просто делаю:

> t = defer`My template is: ${null} and ${null}`;
> t('simple', 'reusable');          // 'My template is: simple and reusable'
> t('obvious', 'late to the party'; // 'My template is: obvious and late to the party'
> t(null);                          // 'My template is: null and undefined'
>
> defer`Choose: ${'ignore'} / ${undefined}`(true, false); // 'Choose: true / false'

Применение этого тега возвращает обратно 'function'(вместо 'string'), который игнорирует любые параметры, переданные в литерал. Затем его можно будет вызвать с новыми параметрами позже. Если параметр не имеет соответствующей замены, он становится 'undefined'.


Расширенный ответ

Этот простой код является функциональным, но если вам нужно более детально проработанное поведение, можно применить ту же логику, и есть бесконечные возможности. Ты мог:

  1. Используйте оригинальные параметры:

Вы можете сохранить исходные значения, переданные литералу в конструкции, и использовать их творчески при применении шаблона. Они могут стать флагами, валидаторами типов, функциями и т. Д. Это пример, в котором они используются как значения по умолчанию:

    function deferWithDefaults([first, ...rest], ...defaults) {
      return (...values) => rest.reduce((acc, curr, i) => {
        return acc + (i < values.length ? values[i] : defaults[i]) + curr;
      }, first);
    }

Затем:

    > t = deferWithDefaults`My template is: ${'extendable'} and ${'versatile'}`;
    > t('awesome');                 // 'My template is: awesome and versatile' 
  1. Напишите фабрику шаблонов:

Сделайте это, заключив эту логику в функцию, которая ожидает в качестве аргумента настраиваемую функцию, которая может быть применена при сокращении (при соединении частей литерала шаблона) и возвращает новый шаблон с настраиваемым поведением.

    const createTemplate = fn => function (strings, ...defaults) {
      const [first, ...rest] = strings;
      return (...values) => rest.reduce((acc, curr, i) => {
        return acc + fn(values[i], defaults[i]) + curr;
      }, first);
    };

Затем вы могли бы, например, написать шаблоны, которые автоматически экранируют или дезинфицируют параметры при написании встроенных html, css, sql, bash ...

    function sqlSanitize(token, tag) {
      // this is a gross simplification, don't use in production.
      const quoteName = name => (!/^[a-z_][a-z0-9_$]*$/.test(name) ? `"${name.replace(/"/g, '""')}"` : name);
      const quoteValue = value => (typeof value == 'string' ? `'${value.replace(/'/g, "''")}'` : value);
      switch (tag) {
        case 'table':
          return quoteName(token);
        case 'columns':
          return token.map(quoteName);
        case 'row':
          return token.map(quoteValue);
        default:
          return token;
      }
    }

    const sql = createTemplate(sqlSanitize);

С помощью этого наивного (повторяю, наивного! ) Шаблона sql мы могли бы строить такие запросы:

    > q  = sql`INSERT INTO ${'table'} (${'columns'})
    ... VALUES (${'row'});`
    > q('user', ['id', 'user name', 'is"Staff"?'], [1, "O'neil", true])
    // `INSERT INTO user (id,"user name","is""Staff""?")
    // VALUES (1,'O''neil',true);`
  1. Принять именованные параметры для замены: несложное упражнение, основанное на том, что уже было дано. В этом другом ответе есть реализация .

  2. Сделать обратный объект ведет себя как 'string': Ну, это спорно, но может привести к интересным результатам. Показано в этом другом ответе .

  3. Разрешить параметры в глобальном пространстве имен на сайте вызова:

Я даю вам многоразовые шаблонные литералы:

Ну, это то , что OP показал его добавление к нему, используя команду evil, я имею в виду, eval. Это можно было бы сделать без него eval, просто выполнив поиск переданного имени переменной в глобальном (или оконном) объекте. Я не буду показывать, как это делать, потому что мне это не нравится. Замыкания - правильный выбор.


2

Это моя лучшая попытка:

var s = (item, price) => {return `item: ${item}, price: $${price}`}
s('pants', 10) // 'item: pants, price: $10'
s('shirts', 15) // 'item: shirts, price: $15'

Чтобы обобщить:

var s = (<variable names you want>) => {return `<template with those variables>`}

Если вы не используете E6, вы также можете:

var s = function(<variable names you want>){return `<template with those variables>`}

Это кажется немного более кратким, чем предыдущие ответы.

https://repl.it/@abalter/reusable-JS-template-literal


2

В целом я против использования зла eval(), но в данном случае это имеет смысл:

var template = "`${a}.${b}`";
var a = 1, b = 2;
var populated = eval(template);

console.log(populated);         // shows 1.2

Затем, если вы измените значения и снова вызовете eval (), вы получите новый результат:

a = 3; b = 4;
populated = eval(template);

console.log(populated);         // shows 3.4

Если вы хотите, чтобы это было в функции, то это можно записать так:

function populate(a, b){
  return `${a}.${b}`;
}

Если вы пишете функцию, включающую шаблон, вам определенно не следует использовать eval.
Берги

@ Берги Почему? Чем он отличается от вашей реализации?
isapir

2
Причины, по которым я «кажется знаю», применимы к любому динамически создаваемому коду. Написание функции так, чтобы она создавала результат без eval()явного вызова, в точности совпадает с eval(), следовательно, в этом нет никакой пользы, так как это только затрудняет чтение кода.
isapir

1
Именно. А поскольку ваша populateфункция не создает код динамически, ее не следует использовать evalсо всеми ее недостатками.
Берги

6
ваша функция могла бы быть просто function populate(a,b) { return `${a}.${b}`; }
оценкой, которая

1

ОБНОВЛЕНО: следующий ответ ограничен одиночными именами переменных, поэтому такие шаблоны, как:, 'Result ${a+b}'не подходят для этого случая. Однако вы всегда можете поиграть со значениями шаблона:

format("This is a test: ${a_b}", {a_b: a+b});

ОРИГИНАЛЬНЫЙ ОТВЕТ:

Основываясь на предыдущих ответах, но создавая более «дружественную» служебную функцию:

var format = (template, params) => {
    let tpl = template.replace(/\${(?!this\.)/g, "${this.");
    let tpl_func = new Function(`return \`${tpl}\``);

    return tpl_func.call(params);
}

Вы можете вызвать его так же:

format("This is a test: ${hola}, second param: ${hello}", {hola: 'Hola', hello: 'Hi'});

И результирующая строка должна быть:

'This is a test: Hola, second param: Hi'

А как насчет такого шаблона? `Result: ${a+b}`
Атирис 06

1
Привет @Atiris, вы правы, это ограничение, я обновил свой ответ.
Роберто

1

Если вы ищете что-то довольно простое (только фиксированные переменные поля, без вычислений, условных операторов ...), но это также работает на стороне клиента в браузерах без поддержки строки шаблона, например IE 8,9,10,11

вот так:

fillTemplate = function (templateString, templateVars) {
    var parsed = templateString;
    Object.keys(templateVars).forEach(
        (key) => {
            const value = templateVars[key]
            parsed = parsed.replace('${'+key+'}',value)
        }
    )
    return parsed
}

Это приведет к поиску каждой переменной. Есть еще один способ, который заменяет сразу все вхождения, который я реализовал в этом модуле: safe-es6-template
Aalex Gabi

1

Меня раздражала дополнительная избыточность, необходимая для ввода текста this.каждый раз, поэтому я также добавил регулярное выражение для расширения таких переменных, как .ato this.a.

Решение:

const interp = template => _thisObj =>
function() {
    return template.replace(/\${([^}]*)}/g, (_, k) =>
        eval(
            k.replace(/([.a-zA-Z0-9$_]*)([a-zA-Z0-9$_]+)/, (r, ...args) =>
                args[0].charAt(0) == '.' ? 'this' + args[0] + args[1] : r
            )
        )
    );
}.call(_thisObj);

Используйте как таковые:

console.log(interp('Hello ${.a}${.b}')({ a: 'World', b: '!' }));
// outputs: Hello World!

1

Я просто публикую один пакет npm, который просто выполняет эту работу. Глубоко вдохновлен этим ответом .

const Template = require('dynamic-template-string');

var tpl = new Template('hello ${name}');

tpl.fill({name: 'world'}); // ==> 'hello world';
tpl.fill({name: 'china'}); // ==> 'hello china';

Его орудие убийственно простое. Желаю вам понравится.


module.exports = class Template {
  constructor(str) {
    this._func = new Function(`with(this) { return \`${str}\`; }`);
  }

  fill(data) {
    return this._func.call(data);
  }
}

1

вы можете использовать встроенную стрелочную функцию, как это, определение:

const template = (substitute: string) => `[^.?!]*(?<=[.?\s!])${substitute}(?=[\s.?!])[^.?!]*[.?!]`;

использование:

console.log(template('my replaced string'));

1

Строка шаблона среды выполнения

var templateString = (template, values) => {
    let output = template;
    Object.keys(values)
        .forEach(key => {
        output = output.replace(new RegExp('\\$' + `{${key}}`, 'g'), values[key]);
    });
    return output;
};

Тест

console.debug(templateString('hello ${word} world', {word: 'wonderful'}));

0

const fillTemplate = (template, values) => {
  template = template.replace(/(?<=\${)\w+(?=})/g, v=>"this."+v);
  return Function.apply(this, ["", "return `"+template+"`;"]).call(values);
};

console.log(fillTemplate("The man ${man} is brother of ${brother}", {man: "John", brother:"Peter"}));
//The man John is brother of Peter

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.