Описание проблемы


Появившиеся в ES6 шаблонные литералы (или шаблонные строки — template literals, template strings) помимо долгожданной интерполяции переменных и выражений принесли возможность вставки многострочного текста без дополнительных ухищрений, усложняющих вид кода.

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

Впрочем, проблемы видны, даже если присмотреться к примерам. Возьмём замечательную статью об этом нововведении из известной серии «ES6 In Depth».

Видите досадные «оспинки»? Лёгкие перекосы в симметрии и стройности?

Маленький пример
var text = (
`foo
bar
baz`)

Большой пример
var html = `<article>
  <header>
    <h1>${title}</h1>
  </header>
  <section>
    <div>${teaser}</div>
    <div>${body}</div>
  </section>
  <footer>
    <ul>
      ${tags.map(tag => `<li>${tag}</li>`).join('\n      ')}
    </ul>
  </footer>
</article>`

Возьмём какой-нибудь простой случай и посмотрим на проблемы внимательнее.

const a = 1;
const b = 2;

console.log(
`a = ${a}.
b = ${b}.`
);

1. Первая кавычка искажает стройность текста, портит выравнивание строк.
2. Из-за смеси элементов литерала и кода, автоматически кажется, будто кавычки попадют в вывод. Приходится дополнительно абстрагироваться от них, чтобы представить, как будет выглядеть окончательный результат.
3. Строчки литерала оказываются вровень с вызовом функции, нарушается привычная структура отступов.

Можно сделать так:

const a = 1;
const b = 2;

console.log(`
  a = ${a}.
  b = ${b}.
`);

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

Но теперь у нас появляются лишние переводы строк и пробелы. Иногда с этим можно смириться, но на универсальное решение не тянет.

Усугубим наш пример введением дополнительных блоков и отступов.

const verbose = true;

if (verbose) {
  console.log(
`const a is ${a}.
const b is ${b}.`
  );
} else {
  console.log(
`a = ${a}.
b = ${b}.`
  );
}

Ужасно. Теперь литерал вообще выпирает слева, разрушая структуру блоков.

Можно исправить описанным выше способом:

if (verbose) {
  console.log(`
    const a is ${a}.
    const b is ${b}.
  `);
} else {
  console.log(`
    a = ${a}.
    b = ${b}.
  `);
}

Стало ещё больше «служебных» пробелов. А если придётся вставлять литерал на ещё более глубоком уровне вложенности? Всё это быстро выйдет из-под контроля.

Присваивания переменным или вызовы console.log можно заменить на функции записи в файлы, дилемма останется той же — или нечитабельная каша, или лишние пробелы и переводы строк:

fs.writeFileSync('log.txt',
`a = ${a}.
b = ${b}.`,
'ascii');

или

fs.writeFileSync('log.txt', `
  a = ${a}.
  b = ${b}.
`, 'ascii');

Я нашёл для себя выход, которым решил поделиться. Не столько потому, что рискнул посчитать его общеполезным, сколько для того, чтобы затеять обсуждение: вполне вероятно, что уже найдены другие выходы, и все желающие смогут их распространить.

Возможное решение


Оно кроется в области того же самого нововведения, а именно в функционале под названием «tagged templates». В уже упомянутой статье есть раздел, посвящённый этому механизму и «разжёвывающий» алгоритм его работы до значительной наглядности: «Demystifying Tagged Templates».

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

//remove auxiliary code spaces in template strings

function xs(strings, ...expressions) {
  const xLF = /^\n|\n$/g;
  const xSP = /\n +/g;

  if (!expressions.length) {
    return strings[0].replace(xSP, '\n').replace(xLF, '');
  } else {
    return strings.reduce((acc, str, i) => (
      (i === 1? acc.replace(xSP, '\n') : acc) +
      expressions[i - 1] +
      str.replace(xSP, '\n')
    )).replace(xLF, '');
  }
}

Или вариант, пригодный для Node.js на то время, пока rest parameters остаются под флагом:

//remove auxiliary code spaces in template strings

function xs(strings) {
  const expressions = Array.from(arguments).slice(1);
  
  const xLF = /^\n|\n$/g;
  const xSP = /\n +/g;

  if (!expressions.length) {
    return strings[0].replace(xSP, '\n').replace(xLF, '');
  } else {
    return strings.reduce((acc, str, i) => (
      (i === 1? acc.replace(xSP, '\n') : acc) +
      expressions[i - 1] +
      str.replace(xSP, '\n')
    )).replace(xLF, '');
  }
}


Как можно заметить, функция убирает по одному начальному и конечному переводу строки из конечного результата, а также удаляет все начальные пробелы в строчках (не затрагивая интерполируемые переменные и выражения).

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

const a = 1;
const b = 2;

console.log(xs`
  a = ${a}.
  b = ${b}.
`);

const verbose = true;

if (verbose) {
  console.log(xs`
    const a is ${a}.
    const b is ${b}.
  `);
} else {
  console.log(xs`
    a = ${a}.
    b = ${b}.
  `);
}


Теперь и код стал более ясным, и в вывод не попадает ничего лишнего.

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

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

Комментарии (59)


  1. vintage
    05.04.2016 09:18

    Вижу тут я антипаттерна три:

    1. Вставка шаблонов в код.
    2. Строковая интерполяция для вставки данных.
    3. Отсутствие экранирования.


    1. Holix
      05.04.2016 10:55

      Вот-вот. Совершенно не совершенный код с костылем!


    1. vmb
      05.04.2016 10:58
      +1

      Почему же это антипаттерны? Например, если я буду использовать это не для серверных скриптов, обрабатывающих чужой пользовательский ввод, а для собственных утилит, скажем, для обработки текста.


      1. vintage
        05.04.2016 11:06
        +1

        Ну вот введёте вы в своей утилите текст, содержащий символ "больше" и весь интерфейс поедет. Оно вам надо?


        1. vmb
          05.04.2016 11:34
          +2

          Но у меня на выходе не HTML. Всё, что нужно, я экранирую в других местах. Экранирование — это совсем другая задача, я не пытался данной функцией решить все возможные проблемы обработки ввода.


          1. vintage
            05.04.2016 11:56

            4) Экранирование вдалеке от места использования.


            1. vmb
              05.04.2016 12:00
              +1

              Я не понимаю, зачем мне экранирование символов, которые в моём случае не являются специальными. Использование JavaScript или Node.js не всегда имеет целью создание HTML.


              1. vintage
                05.04.2016 12:24

                Любой выходной язык требует ту или иную форму экранирования. Даже если вы просто выводите текст в консоль.


                1. vmb
                  05.04.2016 12:37

                  И вы предлагаете мне внести в эту функцию экранирование для всех возможных выходных языков?


                  1. vintage
                    05.04.2016 15:20

                    Вы можете сделать фабрику таких функций.


                    1. vmb
                      05.04.2016 17:51
                      +1

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


  1. Grammka
    05.04.2016 09:53
    +1

    это просто гениально! Как же люди сами до этого не доходят


  1. faiwer
    05.04.2016 10:22
    +1

    Пришёл к чему-то подобному.

    Пример из тестов
    it('section', () => checkMarkBegin('section', renderDOM`
        |#doc
        |    #section^1
        |        #heading "1. txt"
        |        #text "2. txt"*
        |    #section^3
        |        #heading "3. txt"
        `, renderDOM`
        |#doc
        |    #section^1
        |        #heading "1. txt"
        |    #section^2
        |        #heading "2. txt"
        |    #section^3
        |        #heading "3. txt"`));


    1. vmb
      05.04.2016 11:10

      Спасибо. Я сначала пробовал вводить горизонтальные рамки, чтобы отделять многострочные литералы от кода и хоть как-то чинить нарушение структуры отступов, вроде:

      if (verbose) {
        console.log(
      /*`````````````````*/
      `const a is ${a}.
      const b is ${b}.`
      /*`````````````````*/
        );
      } else {
        console.log(
      /*`````````````````*/
      `a = ${a}.
      b = ${b}.`
      /*`````````````````*/
        );
      }

      Но так выходило ещё более громоздко. И проблемы с выравниванием строк самого литерала не решало.


      1. faiwer
        05.04.2016 11:34

        У способа с | я словил одну проблему: я использую повсеместно Tab-ы. А т.к. | завершает indent-часть кода, то последующие за ним Tab-ы каждый редактор рисует как ему хочется. К примеру sublimetext делает с ними что-то совершенно невменяемое (длина таба варьируется в зависимости от расположения Марса). В итоге я до | использую табы, а после (т.е. "внутри шаблонной строки") ? пробелы.


  1. SDI
    05.04.2016 10:29
    +3

    1. Ошибка в коде:
      var firstname = 'Firstname';
      var lastname = 'Lastname';
      console.log(xs`user: ${firstname} ${lastname}`); // 'user: FirstnameLastname' пробел отсутствует

    2. Для каждой переменной в шаблоне создается новый RegExp.
      str.replace(/^ +/mg, '')

    3. Если шаблон маленький, то можно просто написать:
      console.log(`a = ${a}.`);
      console.log(`b = ${b}.`);

      Если же шаблон имеет хоть какой-то размер, то скорей всего он будет в отдельной функции "getMagicTmpl", а не в перемешку с остальным кодом. И в этой функции он будет оформлен как надо.
    4. Итог:
      Есть функция с ошибками, которая не покрывает всех возможных вариантов ("это можно предусмотреть в обрабатывающей функции: удалять лишь часть пробелов"), которая написана далеко не оптимально и не понятен сценарий ее использования в реальном проекте.


    1. faiwer
      05.04.2016 11:31
      +1

      2 Для каждой переменной в шаблоне создается новый RegExp.

      А есть ли разница? Я как-то уже пробовал переиспользовать уже созданные RE в критичных к производительности местах. Никакой разницы в производительности не заметил. Сейчас перепроверил в консоли:
      console


      1. Defff
        05.04.2016 14:57

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


        1. faiwer
          05.04.2016 16:02

          Подозреваю, что дело вовсе не в сложности регулярного выражения. Вероятнее всего, ИМХО, JS-движок сам кеширует эту RE. Разве не так? Во всяком случае это движку ничего не стоит, а выгода очевидна.


          1. Defff
            05.04.2016 18:47

            JS-движок сам кеширует ...
            Он не может всегда кешировать, особенно если есть RegExp c изменяемой переменной
            Вот так у меня даже без изменяемых переменных в регулярке(Просто в js на регулярках собаку cъел, делая много парсеров контента
            console
            console.timer('t1');
            const re = /a{331}/;
            for(i=0;i<100000;i++) 'baab'.match(re);
            console.timerEnd('t1');
            t1: 65.231ms
            
            console.timer('t2');
            const re;
            for(i=0;i<100000;i++) 'baab'.match(/a{331}/);
            console.timerEnd('t2');
            t2: 97.846
            


            1. faiwer
              05.04.2016 19:39
              +1

              • Vivaldi 1.0.435.26: 70 vs 75
              • Chrome 50: 13 vs 11
              • Firefox 45: 105 vs 101
              • nodeJS v5.2.0: 9.7 vs 10

              Одно и то же. Linux Mint 64bit.
              у меня даже без изменяемых переменных в регулярке

              Хм. Подскажите, а что вы имеете ввиду?


              1. Defff
                06.04.2016 04:01

                Хм. Подскажите, а что вы имеете ввиду?
                К примеру:
                for(i=0;i<100000;i++) 'baab'.match(new RegExp(i%5, g))

                Возможно в лине браузеры и кешируют статические регулярки, но на windows различие явное


                1. faiwer
                  06.04.2016 07:32
                  +1

                  new RegExp

                  Ааа. Вот вы про что. А я уж было подумал, что вы про какую-нибудь PERL хитрость с пробросом переменной внутрь уже созданной RE говорите. Ужаснулся даже, чего только не удумали. В случае создания нового RegExp-а из строки проблемы тоже нет, ведь чаще всего такие RE будут разными в каждой итерации, собственно потому и создаются руками.
                  Итак. Windows:

                  Итого: ощутимая разница есть, но в пользу НЕ выноса RE за пределы цикла. Пожалуй, щас jsperf какой-нибудь склепаю.


                  1. Defff
                    06.04.2016 15:57

                    RegExp как объект должен пересоздваться в цикле, если не вынесен за его пределы,

                    <script type="text/javascript">
                    var t1=new Date();
                    var a = "a{331}"; //К примеру эту переменную a получаем из какой-то  функции
                    const re = new RegExp(a,"g");
                    for(i=0;i<100000;i++) 'baab'.match(re);
                    alert(new Date()-t1);
                    
                    
                    var t2=new Date();
                    for(i=0;i<100000;i++) 'baab'.match(RegExp(a,"g"));
                    alert(new Date()-t2);
                    </script>

                    У меня разница примерно на порядок


                    1. faiwer
                      06.04.2016 16:18

                      Deff, мы ходим по кругу. Давайте уже определимся, мы говорим про обычные регулярки, или про new RegExp? Никто вроде и не спорит с тем, что new RegExp нужно выносить за пределы цикла, если она не должна быть уникальной для каждой итерации.
                      Речь ведь идёт о том, что если регулярка обычная, записанная в виде /blabla/, то нет резона выносить её за пределы циклов, т.к. браузеры и без того будут её кешировать, а код будет более читаемым. Т.е. пункта из root-комментария выше, я считаю, из разряда неработающих микрооптимизаций, которые, как известно, — зло.


                      1. Defff
                        06.04.2016 17:06

                        Я сказал про возможное наличие переменной в регулярке, которое кроме как через регесп не вставить(Вы сами спросили что Вы имеете ввиду)
                        Есть еще и запоминаюшие скобки(без регеспов), которые тоже стоит протестировать цикл/не цикл, если у Вас есть такое желание, но во всех справочниках и учебниках определение переменных выносят за пределы цикла, что вроде бы как логично, ибо кеширование переменных браузером в цикле — расчет на наивного пользователя.
                        2. Думаю даже при кешировании разница во времени постоянна, попробуйте увеличить длину цикла на порядок, затем уменьшить. Ибо я пока не понимаю причины даже небольшого уменьшения времени(за которое имхо даже не стоит бороться)


                        1. faiwer
                          06.04.2016 17:55

                          Ибо я пока не понимаю причины даже небольшого уменьшения времени

                          Я право тоже. Но лезть в дебри C++ кода и смотреть почему оно так, мне лень. В одном из своих парсеров, который работает десятки часов напролёт, я думал, что смогу сэкономить хотя бы 5-10% времени именно на выносе всех регулярок из класса куда-нибудь повыше, и дёргать уже экземпляры RegExp-а. Это сильно бы усложнило понимание кода (прямо реально сильно, т.к. регулярки были уникальны и будучи оторванными от контекста...), но я ожидал от этого каких-нибудь явных преимуществ в производительности.
                          Но оказалось что никакой внятной разницы нет. Всё в пределах погрешности. С тех пор к inline-регуляркам я стал относится, как вычисляемым на этапе компиляции значениям, что распространено в системных языках.


                          1. Defff
                            06.04.2016 18:15

                            прямо реально сильно, т.к. регулярки были уникальны и будучи оторванными от контекста...),
                            При наличии хорошего очевидного названия переменной для регулярки — проблемы вроде как не стоит, тем паче вынесение переменных перед циклом, — стандартная практика.
                            2. Мой первый пример с цифрами для Оперы 12.17 (Часто ей пользуюсь, ибо на поддержке нужна правка скриптов прямо в контенте страницы, чего нет у остальных браузеров, проверил сейчас в Лисе, цифры примерно соответствуют Вашим)


                            1. faiwer
                              06.04.2016 18:34

                              тем паче вынесение переменных перед циклом

                              С простым циклом — соглашусь. А если это экземпляр класса? Внутри этого класса пол-сотни методов, и в большей части из них есть специфичекие регекспы? Единственное место куда их можно вынести — за пределы этих методов. К примеру в конструктор класса, или выше по коду.
                              Учитывая, что эти RE являются чуть ли не сутью данного класса (парсер, что с него взять), то это очень сильно ухудшает читаемость кода.
                              Однако производительность в данном случае стоит того, поэтому я, скрепя сердцем, готов был пойти на такой шаг. Но оказалось не нужным.
                              При наличии хорошего очевидного названия переменной для регулярки

                              Именование это самая главная проблема программирования в целом :) А разумное именование для RE это проблема в квадрате. Должно быть вы шутите :D
                              Мой первый пример с цифрами для Оперы 12.17

                              Ого. Последний из могикан?


                              1. Defff
                                06.04.2016 19:53

                                Ого. Последний из могикан?

                                На поддержке пользователи часто просят править скрипт в теле своего шаблона сайта, в ином другом браузере, особенно с аяксами и фреймами сделать это чрезвычайно трудно, в Опере это правиться прямо редакцией HTML кода страницы (cntr U и правка)


                1. faiwer
                  06.04.2016 07:38
                  +1

                  jsPerf. Разницу >5% показал только в IE9.


                  1. faiwer
                    06.04.2016 07:57
                    +1

                    результат
                    image


    1. vmb
      05.04.2016 11:31

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


  1. vital72
    05.04.2016 10:59

    вот это костылище!
    удивительно, что C-like язык не предоставляет такой же возможности для записи длинной строки в несколько строк, как это реализовано в самом C.


    1. Shannon
      05.04.2016 11:24
      +1

      В C есть 2 способа писать строки в несколько строк, но они ничем особо не отличаются от вариантов в js:

      var jsstr =
          "test" +
          "middle" +
          "end";

      Или
      var jsstr = "testmiddleend";

      Но тут, как минимум, не просто запись в несколько строк, а template literals


      1. vital72
        05.04.2016 11:35

        ну да, не отличаются, лишних пробелов не будет и переводов строк.
        а "+" для С не нужен


        1. vital72
          05.04.2016 11:39

          все ж таки в С проще — не нужна операция сложения


          1. Shannon
            05.04.2016 12:12
            +1

            Не нужна операция сложения, но нужно открывать и закрывать кавычки, поэтому это не сильно отличается от js варианта с +
            В случае с `` как раз ни кавычки, ни сложение не требуется, тем и удобен
            Например, для шаблонизатора где учитываются отступы для формирования dom, можно сделать так

            var html = template.compile(`
            extends layout
            
            block main
                include menu
            
                ul.submenu
                    li.item home
                    li.item about
            
            block footer
                 .end footer text
            `);

            Соответственно, даже вариант без сложения, с постоянной расстановкой кавычек был бы уже не удобен


        1. faiwer
          05.04.2016 11:40
          +1

          ну да, не отличаются, лишних пробелов не будет и переводов строк.

          Правда? Я не знаю C, вопрос:
          void fn() 
          {
            if(some)
              char *str = "\n
              line1\n
               line2\n
              line3";
          }

          Будет ли str равным \nline1\n line2\nline3?


          1. vital72
            05.04.2016 11:48

            вообще-то, когда я писал свой первый комментарий я имел в виду разбиение длинной строки так, чтобы не надо было части склеивать операцией конкатенации, именно этот синтаксис отсутствует в js и в php, кстати, тоже


            1. vmb
              05.04.2016 11:53
              +1

              Приведите, пожалуйста, пример кода и его результирующий вывод.


              1. vital72
                05.04.2016 12:08

                ну вот как-то так http://cpp.sh/862m


                1. vmb
                  05.04.2016 12:17
                  +1

                  Но здесь же совсем разные задачи решаются. В вашем случае — разбиение длинной строки на сегменты, при этом переводы строк в коде не соответствуют переводам строк в выводе. В случае шаблонных литералов в JS — попытка добиться максимального соответствия между шаблоном и выводом. Что видим в коде, получим в выводе. Проблема была в том, чтобы это соответствие ещё как-то вписать в структуру кода.


                  1. vital72
                    05.04.2016 12:28

                    ну да, переводы строк надо выставлять самостоятельно и мне так привычнее


                1. faiwer
                  05.04.2016 12:44
                  +2

                  Это не одно и тоже. У вас в каждой строке необходимо указывать кавычки, а каждый перевод строки задавать в виде \n. Тоже самое, но с +-ми в JS было и ранее, но мало кто считал это удобным для много-строчного текста :)


          1. Holix
            05.04.2016 12:11

            Под linux str будет равным "\n\n line1\n\n line2\n\n line3", т.е. не экранированые переводы строк тоже добавятся. (Под windows будет "\n\r\n ....")


            1. faiwer
              05.04.2016 12:50
              +1

              Звучит удобно. Ведь руками \n можно и не указывать. Получается что нагромождение пробелов слева, из-за indent-а, не учитывается?
              UPD. не обратил внимание, что у вас они есть, просто отобразились как один пробел. Тогда выходит, что ровно те же самые проблемы.


  1. SDI
    05.04.2016 17:17
    +1

    Как альтернатива:

    function xs(str){
      return str.replace(/\n +/g, '\n').replace(/^\n|\n$/g, '');
    }
    
    const a = 1;
    const b = 2;
    
    console.log(xs(`
      a = ${a}.
      b = ${b}.
    `));
    


    1. vmb
      05.04.2016 17:44
      +1

      Да, я думал о таком варианте. Но он более рискованный, так как затронет интерполируемые выражения. А это может быть нецелесообразно в подобных случаях:

      fs.writeSync(logFile, xs`
        ${new Date()}
        ${Possibly_Multiline_Text_With_Inndents}
      
      `, 'ascii');
      


  1. Defff
    05.04.2016 19:15
    +2

    Раз пошли в ход такие конструкции, возможно стоит вспомнить такой костыль?
    Имхо он заодно и отделяет контент такой текстовой константы от остального

    function textConst (){ /*==123==;
      <style type="text/css">
      #pun .main{
         display:none;
       }
      </style>
      <div id=world>
        Привет Мир!
      </div>
    ==123==;*/
    } textConst = textConst.toString().split('==123==;')[1];
    alert(textConst)


    1. vmb
      05.04.2016 19:32

      Да уж, воистину голь на выдумки хитра)


  1. v1vendi
    05.04.2016 19:27
    +1

    Есть вопрос, может кто-нибудь сможет мне подсказать? Есть ли возможность на основании template strings теперь как-то заменить функционал underscore или mustache? Я так и не нашёл функцию, позволяющую создать template string из стороннего объекта, например загруженного через AJAX файла. Пример:

    файл template.html
    $(name)

    скрипт
    fetch('template.html').then(_=>_.text()).then(templateString=>{
    compileTemplate(templateString, {name:'Вася'});
    })

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


    1. faiwer
      05.04.2016 19:45
      +2

      Если я правильно понял, то вы можете разобрать строку на составляющие самостоятельно, а затем скормить её String.raw. Правда если интерполируемые значения могут иметь сложную структуру, то лучше даже не начинать.
      А по сути template string это часть языка. Т.е. JS парсер, натыкаясь на такую строку ещё при разборе разбивает её на составляющие и в AST уходят уже куски, а не вся целиком. Едва ли такая часть парсера будет доступна снаружи.


    1. vmb
      05.04.2016 20:19
      +1

      Наивная функция может быть такой:

      const str = 'A: ${1+2}\nB: ${3+4}';
      
      function compileTL(str) {
        const strings = str.split(/\$\{.*?\}/);
        const expressions = str.match(/\$\{.*?\}/g).map(expr => eval(expr.replace(/^\$\{|\}$/g, '')));
        return String.raw({ raw: strings }, ...expressions);
      }
      
      console.log(compileTL(str));
      

      Вывод:

      A: 3
      B: 7

      Но боюсь, что такой парсинг много чего не учитывает. Например, вложенные шаблонные строки сразу же всё усложнят.


      1. vmb
        05.04.2016 21:51

        Поправка. Поскольку пустые выражения вызывают ошибку, даже наивная регулярка должна быть такой в обоих случаях (не *, а +): /\$\{.+?\}/


    1. vmb
      07.04.2016 20:32

      Не уверен, что это именно то, что нужно, но вроде бы на близкую тему.


      1. v1vendi
        08.04.2016 11:45
        +1

        в статье Арчибальда немножко другое — тут асинхронные шаблоны, содержимое которых изменяется в процессе подгрузки контента, но спасибо за наводку


  1. CrazyNiger
    06.04.2016 19:37
    +2

    Круто конечно, но только вот в вашем «большом примере» функция съест все ведущие пробелы, и нарушит форматирование html-кода. А если его нужно сохранить, тогда литерал будет выбиваться влево из основного кода.


    1. vmb
      06.04.2016 19:39

      Да, я об этом писал в конце статьи. Для таких случаев можно создать функцию с ограничителями. Например, посчитать, сколько пробелов добавлено в первую строчку перед корневым тегом дерева — они и будут служебными. Из начала следующих строчек потом можно удалить только их, так все отступы останутся.