'use strict' function tag(literals, ...values) { // Tag function used with template strings that applies various convenient // modifications to multiline strings, including: // // * Joins literals and embedded values of a template string while keeping // the indentation from the literals (see joinTemplateString) // // * Removes the greatest amount of indentation from the beginning of each // line, such that the indentation differences between each line are kept // (see whitespaceAtBeginningMultiline) // // * Removes whitespace-only lines at the beginning and end of the string // (see removeInitialWhitespaceLines and removeLeadingWhitespaceLines) let resultLines = joinTemplateString(literals, ...values).split('\n') // Reduce whitespace from beginning of lines const minWhitespace = whitespaceAtBeginningMultiline(literals.join('')) resultLines = resultLines.map(line => line.slice(minWhitespace)) // Remove whitespace lines at beginning and end let result = resultLines.join('\n') result = removeInitialWhitespaceLines(result) result = removeLeadingWhitespaceLines(result) return result } function joinTemplateString(literals, ...values) { // Joins the literals and values of a template string while maintaining // indentation passed from literals. For example, using this template: // // ` // // ` // // ..and given the array ['A', 'B', 'C'] for items, this result would be // gotten: // // ` // // ` let resultLines = [] for (let literalI = 0; literalI < literals.length; literalI++) { const literal = literals[literalI] const literalLines = literal.split('\n') let curResultLines = literalLines.slice(0) const value = values[literalI] if (value) { const lastLiteralLine = literalLines[literalLines.length - 1] const lastResultLine = resultLines[resultLines.length - 1] || '' // Normally we just add the whitespace from the last line of the current // literal. However, if there are multiple literals corresponding to a // single line of the tag input string, we want to use the whitespace // from the latest result line - because the literal won't contain any // of the result's indent! const whitespaceAmountLit = whitespaceAtBeginning(lastLiteralLine) const whitespaceAmountRes = whitespaceAtBeginning(lastResultLine) const whitespaceAmountMax = Math.max(whitespaceAmountLit, whitespaceAmountRes) const [first, ...rest] = value.toString().split('\n') const modified = [first, ...rest.map( line => ' '.repeat(whitespaceAmountMax) + line )] curResultLines = squishArrays(curResultLines, modified) } resultLines = squishArrays(resultLines, curResultLines) } return resultLines.join('\n') } function removeInitialWhitespaceLines(lines) { // Removes whitespace-only lines from the beginning of a string. const result = lines.split('\n') while (result.length) { if (isOnlyWhitespace(result[0])) { result.shift() } else { break } } return result.join('\n') } function removeLeadingWhitespaceLines(lines) { // Removes whitespace-only lines from the ending of a string. const result = lines.split('\n') while (result.length) { if (isOnlyWhitespace(result[result.length - 1])) { result.pop() } else { break } } return result.join('\n') } function squishArrays(arr1, arr2) { // "Squishes" two string arrays together. Basically works like a concat, but // with the first item of arr2 being concatenated to the last item of arr1. const [first, ...rest] = arr2 const result = arr1.slice(0, -1) const lastOfArr1 = arr1[arr1.length - 1] result.push((lastOfArr1 || '') + first) result.push(...rest) return result } function whitespaceAtBeginningMultiline(str) { // Gets the minimum number of whitespace characters at the beginning of each // of the lines of a multiline string. For example, in this string: // // ` // Hello // World // !!!!! // ` // // The value 1 would be returned, for the one whitespace before "!!!!!". // // Note that this function ignores whitespace-only lines. Called on this // string, where each dot is a space, it will return the value 4: // // ` // ....Hi // .......There // .. // ....Friendo! // ` // // Though the amount of whitespace on the third line is 2, the function // ignores this line, since it has no non-whitespace characters. // // In a string composed entirely of whitespace, the longest line length // is returned. const lines = str.split('\n') return lines.reduce( (min, line) => { if (isOnlyWhitespace(line)) { return min } else { return Math.min(min, whitespaceAtBeginning(line)) } }, Math.max(...lines.map(line => line.length)) ) } function isOnlyWhitespace(str) { // Returns whether a passed string is composed solely of whitespace. // Works only on single-line strings, as newlines are not considered // whitespace. return whitespaceAtBeginning(str) === str.length } function whitespaceAtBeginning(str) { // Gets the number of whitespace characters at the beginning of a string. for (let i = 0; i < str.length; i++) { if (!isWhitespaceCharacter(str[i])) { return i } } return str.length } function isWhitespaceCharacter(char) { // Returns whether a passed character is a whitespace character or not. // May be expanded. Intentionally does not include line break characters. return [' '].includes(char) } if (require.main === module) { const items = [1, 2, 3] const generateSitePage = (head, content) => tag` (Begin head..)${head}(..End head)
${"Early insert!"} (Begin content x1..)${content}(..end content; begin content x2..)${content}(..end content!) ${"And back down."}
` console.log(generateSitePage( tag` Hello, world! `, tag` ` )) } if (typeof module === 'object' && typeof module.exports === 'object') { module.exports = tag }