Conveniently insert multi-line template literals into JavaScript code

    Description of the problem


    The template literals that appeared in ES6 (or template literals, template strings) in addition to the long-awaited interpolation of variables and expressions made it possible to insert multi-line text without additional tricks that complicate the look of the code.

    However, what looks beautiful in various examples on this topic in real code is sometimes clothed in a new kind of disgrace.

    However, problems are visible, even if you look closely at the examples. Let's take a wonderful article about this innovation from the famous ES6 In Depth series.

    See the annoying little pockmarks? Slight distortions in symmetry and harmony?

    Small example
    var text = (
    `foo
    bar
    baz`)
    

    Great example
    var html = `

    ${title}

    ${teaser}
    ${body}
      ${tags.map(tag => `
    • ${tag}
    • `).join('\n ')}
    `

    Take a simple case and look at the problems more closely.

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

    1. The first quotation mark distorts the harmony of the text, spoils the alignment of lines.
    2. Due to the mixture of literal elements and code, it automatically seems as if quotation marks get into output. We have to further abstract from them to imagine what the final result will look like.
    3. The literal lines are flush with the function call, the usual indentation structure is violated.

    You can do this:

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

    This solves the aforementioned problems: line alignment is preserved, code and literal elements are spaced, the usual indentation structure improves readability, and separates function and arguments more clearly.

    But now we have extra line breaks and spaces. Sometimes you can come to terms with this, but you don’t want a universal solution.

    To exacerbate our example, the introduction of additional blocks and indents.

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

    Awful. Now the literal generally sticks out to the left, destroying the structure of the blocks.

    You can fix it as described above:

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

    There are even more “service” gaps. And if you have to insert a literal at an even deeper level of nesting? All this will quickly get out of hand.

    Assignments to variables or calls console.logcan be replaced with functions for writing to files, the dilemma will remain the same - either an unreadable mess, or extra spaces and line breaks:

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

    or

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

    I found a way out for myself, which I decided to share. Not so much because I ventured to find it generally useful, but to start a discussion: it is likely that other solutions have already been found, and everyone can distribute them.

    Possible Solution


    It lies in the field of the same innovation, namely in the functionality called “tagged templates”. In the already mentioned article, there is a section devoted to this mechanism and “chewing” the algorithm of its operation to a significant clarity: “ Demystifying Tagged Templates ”.

    The "bones" of functions that process template literals given by the author led me to think of using something similar to remove all service spaces and line feeds from multi-line literals. This function turned out:

    //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, '');
    }
    


    As you can see, the function removes one starting and ending line feed from the final result, and also removes all leading spaces in the lines (without affecting the interpolated variables and expressions). At the same time, she tries to preserve the internal indentation of deeper attachments: on the first line, she defines the official indentation in the code and removes only the equal number of spaces - this makes it possible to save structures like the above large example with HTML code.

    Now you can safely use our more readable options, with a small, barely noticeable addition that does not spoil the look of the code (however, the function can be called anything - longer, shorter, using different options for clarity, intuitive, etc.):

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


    Now the code has become clearer, and nothing superfluous gets to the output.

    I hope this is only the first test case, and other ideas will appear that differ more or less radically. Perhaps, unobvious obstacles to the use of such solutions will also be found, undetected errors will be fixed, unaccounted cases of use that violate the function's operation will be indicated.

    PS About a simple library that solves similar problems.

    PPS Updated the function to save nested indents.

    Also popular now: