Описание проблемы
Появившиеся в 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. Обновил функцию для сохранения вложенных отступов.