Удобная вставка многострочных шаблонных литералов в код на JavaScript

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


    Появившиеся в 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 indent = new RegExp(`\n {${strings[0].match(/\n+( *)/)[1].length}}`, 'g');
    
      return expressions.reduce(
        (acc, expr, i) => `${acc}${expr}${strings[i + 1].replace(indent, '\n')}`,
        strings[0].replace(indent, '\n')
      ).replace(/^\n|\n$/g, '');
    }
    


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

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

    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}.
      `);
    }
    


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

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

    P.S. О простой библиотеке, решающей аналогичные проблемы.

    P.P.S. Обновил функцию для сохранения вложенных отступов.

    Комментарии 61

      0
      Вижу тут я антипаттерна три:
      1. Вставка шаблонов в код.
      2. Строковая интерполяция для вставки данных.
      3. Отсутствие экранирования.
        0
        Вот-вот. Совершенно не совершенный код с костылем!
          +1
          Почему же это антипаттерны? Например, если я буду использовать это не для серверных скриптов, обрабатывающих чужой пользовательский ввод, а для собственных утилит, скажем, для обработки текста.
            +1
            Ну вот введёте вы в своей утилите текст, содержащий символ "больше" и весь интерфейс поедет. Оно вам надо?
              +2
              Но у меня на выходе не HTML. Всё, что нужно, я экранирую в других местах. Экранирование — это совсем другая задача, я не пытался данной функцией решить все возможные проблемы обработки ввода.
                0
                4) Экранирование вдалеке от места использования.
                  +1
                  Я не понимаю, зачем мне экранирование символов, которые в моём случае не являются специальными. Использование JavaScript или Node.js не всегда имеет целью создание HTML.
                    0
                    Любой выходной язык требует ту или иную форму экранирования. Даже если вы просто выводите текст в консоль.
                      0
                      И вы предлагаете мне внести в эту функцию экранирование для всех возможных выходных языков?
                        0
                        Вы можете сделать фабрику таких функций.
                          +1
                          В принципе, это имело бы смысл, если бы я писал универсальную библиотеку на все случаи жизни. Но пока это сильно уведёт нас в сторону. Функция ведь и так открыта для любых добавлений. Никому ничего не мешает добавить в строчку, конкатенирующую выражение, любые дополнительные функции для экранирования, проверки ограничений и т.д.
          +1
          это просто гениально! Как же люди сами до этого не доходят
            +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"`));


            В данном случае renderDom делает гораздо больше, чем просто поправляет отступы и убирает лишние переносы строк. Но, касаемо самого текста, суть примерно та же. Для большей очевидности и наглядности каждую строку начинаю с |.
            Правда шаблонные литералы не через String.raw за пределами тестов мне пока не пригодилось. А вот в них позволили очень сложные вырвиглазные вещи описать, используя в 10 раз меньше кода и улучшив раз в 5 наглядность. Правда попутно сочиняя новый язык шаблонов, но это уже другая история :)
              0
              Спасибо. Я сначала пробовал вводить горизонтальные рамки, чтобы отделять многострочные литералы от кода и хоть как-то чинить нарушение структуры отступов, вроде:
              if (verbose) {
                console.log(
              /*`````````````````*/
              `const a is ${a}.
              const b is ${b}.`
              /*`````````````````*/
                );
              } else {
                console.log(
              /*`````````````````*/
              `a = ${a}.
              b = ${b}.`
              /*`````````````````*/
                );
              }

              Но так выходило ещё более громоздко. И проблемы с выравниванием строк самого литерала не решало.
                0
                У способа с | я словил одну проблему: я использую повсеместно Tab-ы. А т.к. | завершает indent-часть кода, то последующие за ним Tab-ы каждый редактор рисует как ему хочется. К примеру sublimetext делает с ними что-то совершенно невменяемое (длина таба варьируется в зависимости от расположения Марса). В итоге я до | использую табы, а после (т.е. "внутри шаблонной строки") ― пробелы.
              +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
                2 Для каждой переменной в шаблоне создается новый RegExp.

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


                3… И в этой функции он будет оформлен как надо.

                Если бы. Там возникнут те же самые проблемы.
                  0
                  Слишком простая регулярка, при сложных и длинных регулярках с второй пример будет значительно дольше
                    0
                    Подозреваю, что дело вовсе не в сложности регулярного выражения. Вероятнее всего, ИМХО, JS-движок сам кеширует эту RE. Разве не так? Во всяком случае это движку ничего не стоит, а выгода очевидна.
                      0
                      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
                        • 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.
                        у меня даже без изменяемых переменных в регулярке

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

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

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

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

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

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

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

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

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

                                          На поддержке пользователи часто просят править скрипт в теле своего шаблона сайта, в ином другом браузере, особенно с аяксами и фреймами сделать это чрезвычайно трудно, в Опере это правиться прямо редакцией HTML кода страницы (cntr U и правка)
                              +1
                              jsPerf. Разницу >5% показал только в IE9.
                                +1
                                результат
                                image

                                Вот это поворот. Safari на MacBook AIR вырвался вперёд обойдя куда более быстрый десктоп :) У вас точно Windows? Похоже, что в Safari вынос RE действительно полезен.
                    0
                    Спасибо большое. Ошибку, кажется, исправил, создание регулярных выражений вынес из цикла (можно их и из функции вынести в глобальные переменные, но это уже будет, наверное, преждевременная оптимизация для моих задач).
                    Сценарий использования в моём случае такой: я делаю локальные копии сетевых словарей при помощи Node.js или NW.js, поэтому в скриптах постоянно приходится записывать большие блоки текста в файл. Блоки многострочные, состоящие частично из моих строк (в том числе с разметкой языка для словарной оболочки), частично из переменных, содержащих извлечённую из сетевых страниц информацию.
                    0
                    вот это костылище!
                    удивительно, что C-like язык не предоставляет такой же возможности для записи длинной строки в несколько строк, как это реализовано в самом C.
                      +1
                      В C есть 2 способа писать строки в несколько строк, но они ничем особо не отличаются от вариантов в js:
                      var jsstr =
                          "test" +
                          "middle" +
                          "end";

                      Или
                      var jsstr = "\
                      test\
                      middle\
                      end";

                      Но тут, как минимум, не просто запись в несколько строк, а template literals
                        0
                        ну да, не отличаются, лишних пробелов не будет и переводов строк.
                        а "+" для С не нужен
                          0
                          все ж таки в С проще — не нужна операция сложения
                            +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
                            ну да, не отличаются, лишних пробелов не будет и переводов строк.

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

                            Будет ли str равным \nline1\n line2\nline3?
                              0
                              вообще-то, когда я писал свой первый комментарий я имел в виду разбиение длинной строки так, чтобы не надо было части склеивать операцией конкатенации, именно этот синтаксис отсутствует в js и в php, кстати, тоже
                                +1
                                Приведите, пожалуйста, пример кода и его результирующий вывод.
                                  0
                                  ну вот как-то так http://cpp.sh/862m
                                    +1
                                    Но здесь же совсем разные задачи решаются. В вашем случае — разбиение длинной строки на сегменты, при этом переводы строк в коде не соответствуют переводам строк в выводе. В случае шаблонных литералов в JS — попытка добиться максимального соответствия между шаблоном и выводом. Что видим в коде, получим в выводе. Проблема была в том, чтобы это соответствие ещё как-то вписать в структуру кода.
                                      0
                                      ну да, переводы строк надо выставлять самостоятельно и мне так привычнее
                                      +2
                                      Это не одно и тоже. У вас в каждой строке необходимо указывать кавычки, а каждый перевод строки задавать в виде \n. Тоже самое, но с +-ми в JS было и ранее, но мало кто считал это удобным для много-строчного текста :)
                                  0
                                  Под linux str будет равным "\n\n line1\n\n line2\n\n line3", т.е. не экранированые переводы строк тоже добавятся. (Под windows будет "\n\r\n ....")
                                    +1
                                    Звучит удобно. Ведь руками \n можно и не указывать. Получается что нагромождение пробелов слева, из-за indent-а, не учитывается?
                                    UPD. не обратил внимание, что у вас они есть, просто отобразились как один пробел. Тогда выходит, что ровно те же самые проблемы.
                            +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
                              Да, я думал о таком варианте. Но он более рискованный, так как затронет интерполируемые выражения. А это может быть нецелесообразно в подобных случаях:

                              fs.writeSync(logFile, xs`
                                ${new Date()}
                                ${Possibly_Multiline_Text_With_Inndents}
                              
                              `, 'ascii');
                              
                              +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)
                                0
                                Да уж, воистину голь на выдумки хитра)
                                +1
                                Есть вопрос, может кто-нибудь сможет мне подсказать? Есть ли возможность на основании template strings теперь как-то заменить функционал underscore или mustache? Я так и не нашёл функцию, позволяющую создать template string из стороннего объекта, например загруженного через AJAX файла. Пример:

                                файл template.html
                                $(name)

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

                                все актуальные шаблонизаторы строят функцию, которую потом выполняют через eval. Есть ли вариант в этом примере реализовать функцию compileTemplate, которая будет использовать шаблонные строки?
                                  +2
                                  Если я правильно понял, то вы можете разобрать строку на составляющие самостоятельно, а затем скормить её String.raw. Правда если интерполируемые значения могут иметь сложную структуру, то лучше даже не начинать.
                                  А по сути template string это часть языка. Т.е. JS парсер, натыкаясь на такую строку ещё при разборе разбивает её на составляющие и в AST уходят уже куски, а не вся целиком. Едва ли такая часть парсера будет доступна снаружи.
                                    +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

                                    Но боюсь, что такой парсинг много чего не учитывает. Например, вложенные шаблонные строки сразу же всё усложнят.
                                      0
                                      Поправка. Поскольку пустые выражения вызывают ошибку, даже наивная регулярка должна быть такой в обоих случаях (не *, а +): /\$\{.+?\}/
                                      0
                                      Не уверен, что это именно то, что нужно, но вроде бы на близкую тему.
                                        +1
                                        в статье Арчибальда немножко другое — тут асинхронные шаблоны, содержимое которых изменяется в процессе подгрузки контента, но спасибо за наводку
                                      +2
                                      Круто конечно, но только вот в вашем «большом примере» функция съест все ведущие пробелы, и нарушит форматирование html-кода. А если его нужно сохранить, тогда литерал будет выбиваться влево из основного кода.

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

                                          Обновил функцию для сохранения вложенных отступов.

                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                          Самое читаемое