Зручна вставка багаторядкових шаблонних літералів код на 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) {
if (!expressions.length) {
return strings[0].replace(/^ +/mg, ").replace(/^\n|\n$/g, ");
} else {
return strings.reduce((acc, str, i) => {
return (
(i === 1? acc.replace(/^ +/mg, ") : acc) +
expressions[i - 1] +
str.replace(/^ +/mg, ")
);
}).replace(/^\n|\n$/g, ");
}
}

Або варіант, придатний для Node.js на той час, поки rest parameters залишаються під прапором:

//remove auxiliary code spaces in template strings

function xs(strings) {
const expressions = Array.from(arguments).slice(1);

if (!expressions.length) {
return strings[0].replace(/^ +/mg, ").replace(/^\n|\n$/g, ");
} else {
return strings.reduce((acc, str, i) => {
return (
(i === 1? acc.replace(/^ +/mg, ") : acc) +
expressions[i - 1] +
str.replace(/^ +/mg, ")
);
}).replace(/^\n|\n$/g, ");
}
}


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

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

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), це можна передбачити в обробній функції: видаляти лише частину прогалин (наприклад, кількість, яке зустрічається лише в першій сходинці), використовувати для різних цілей різні види пробільних символів (табуляцію, нерозривні пробіли і т. д.).

Сподіваюся, це тільки перший пробний приклад, і з'являться інші ідеї, що відрізняються більш або менш радикально. Можливо, також будуть знайдені неочевидні перешкоди до використання подібних рішень, виправлені непомічені помилки, вказані невраховані випадки використання, порушують роботу функції.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.