How can I have multiline strings that don't break indentation in JavaScript?
Asked Answered
I

8

95

According to this esdiscuss discussion, it is possible in ECMAScript 6 to define multiline strings without having to place subsequent lines of the string at the very beginning of the line.

Allen Wirfs-Brock’s post contains a code example:

var a = dontIndent
        `This is a template string.
         Even though each line is indented to keep the
         code neat and tidy, the white space used to indent
         is not in the resulting string`;

Could someone explain how this can be achieved? How to define this dontIndent thing in order to remove the whitespace used for indentation?

Illumine answered 18/9, 2014 at 23:29 Comment(4)
There are now multiple modules on npm for this, most can be found via a keyword search for "dedent"Ameliorate
The bounty expires soon. Is there an update to this answer?Rachealrachel
@Pureferret I created a Babel plugin that does not require putting any tag (function name) before the template literal, preserves newlines but gets rid of the leading whitespace at compile-time. Maybe it would better suit your needs? More details are on my answer.Hardball
the dedent package has worked great for meMezzorilievo
B
55

2020 answer: there is still nothing built into the JS stdlib to handle de-denting long lines, although TC39 has discussed adding a new template literal that handles indentation. You have 2 options presently:

  1. The endent and dedent-js packages will handle this. Note the dedent-js package actually works with both tabs and spaces.
    var dedent = require('dedent-js');
    var text = dedent(`
      <div>
        <span>OK</span>
        <div>
          <div></div>
        </div>
      </div>
    `);

Will strip out the proceeding whitespace on each line and the leading carriage return. It also has more users, an issue tracker, and is more easily updated than copypasting from Stack Overflow!

Note: dedent is a separate package from dedent-js and dedent fails on tabs.

  1. Don't indent long lines, but use an editor that shows long lines as indented. Eg, vsCode - you can simply use long lines, not indent anything, and include carriage returns in a long string. vsCode will show them indented. The string below has no indentation - the second line The empty export... is immediately after the carriage return, but shows up as indented.

enter image description here

Barbarossa answered 18/8, 2016 at 13:30 Comment(5)
The bounty expires soon. Is there an update to this answer?Rachealrachel
@Pureferret alas this still isn't in the stdlib. I suspect the reason why TC39 doesn't have a dedent (that looks at the starting indent of the multiline string is because doing this (like dedent) is a mix of compile-time and run-time features. However I'll add two additional solutions to the answer above,Barbarossa
Thanks for adding those in. I did see a post on TC39 for decent:es.discourse.group/t/… as well as the endent package. Let me know if you want to add this into your answer.Rachealrachel
for the second option you need to have "editor.wordWrap": "on" in your settings. I know this is obvious, but the name of the setting can help a few people looking to enable itSauncho
@Barbarossa This can be done purely on compile-time. I created a Babel plugin that does not require putting any tag (function name) before the template literal, preserves newlines but gets rid of the leading whitespace at compile-time. It preserves empty lines as well. Maybe it would better suit your needs? More details are on my answer.Hardball
E
28

You can also just do a string replace for double spaces (assuming your indentation uses spaces, not tabs). Obviously any double spaces in your actual string would be removed, but for most cases this should be ok.

const MSG = (`Line 1
          line 2
          line 3`).replace(/  +/g, '');
// outputs
/*
Line 1
line 2
line 3
*/
Eurythmic answered 5/12, 2018 at 0:9 Comment(3)
The bounty expires soon. Is there an update to this answer?Rachealrachel
If you add ^, so /^ +/g, then it'll only remove line-leading spaces. Might need a multi-line flag.Acuate
This is okay but Tomas Langkaas's answer preserves white consecutive spaces if they aren't in the beginning of the lines which is better for some use cases.Parathion
I
18

This feature is implemented by defining a custom function and then using it as a tag (dontIndent above). The code blow is from Zenparsing's gist:

function dedent(callSite, ...args) {

    function format(str) {

        let size = -1;

        return str.replace(/\n(\s+)/g, (m, m1) => {

            if (size < 0)
                size = m1.replace(/\t/g, "    ").length;

            return "\n" + m1.slice(Math.min(m1.length, size));
        });
    }

    if (typeof callSite === "string")
        return format(callSite);

    if (typeof callSite === "function")
        return (...args) => format(callSite(...args));

    let output = callSite
        .slice(0, args.length + 1)
        .map((text, i) => (i === 0 ? "" : args[i - 1]) + text)
        .join("");

    return format(output);
}

I've successfully tested it in Firefox Nightly:

enter image description here

Illumine answered 19/9, 2014 at 15:27 Comment(4)
In case this seems confiusing for someone, you basically can call a js function by writing its name followed by <code>``</code> (Tagged templates) . The function then has access to the contents of the template literal.Moreira
Note that this function only strips away the amount of indentation as indicated by the first line with indentation. If a line contains more indentation after that first line, it keeps that.Mayne
This solution seems to have problem with empty linesDesimone
@Desimone I created a Babel plugin that does not require putting any tag (function name) before the template literal, preserves newlines but gets rid of the leading whitespace at compile-time. It preserves empty lines as well. Maybe it would better suit your needs? More details are on my answer.Hardball
T
13

How to define this dontIndent thing in order to remove the whitespace used for indentation?

I suppose something like this should suffice for many cases (including the OP):

function dontIndent(str){
  return ('' + str).replace(/(\n)\s+/g, '$1');
}

Demo code in this snippet:

var a = dontIndent
        `This is a template string.
         Even though each line is indented to keep the
         code neat and tidy, the white space used to indent
         is not in the resulting string`;

console.log(a);
         
function dontIndent(str){
  return ('' + str).replace(/(\n)\s+/g, '$1');
}

Explanation

JavaScript template literals can be called with a tag, which in this example is dontIndent. Tags are defined as functions, and are called with the template literal as an argument, so we define a dontIndent() function. The template literal is passed as an argument in an array, so we use the expression ('' + str) to cast the array content to a string. Then, we can use a regular expression like /(\n)\s+/g to .replace() all occurrences of line breaks followed by white space with only the line break to achieve the aim of the OP.

Tallis answered 10/8, 2020 at 19:6 Comment(5)
How can I change your RegEx if I still want the line-breaks except the indentations?Tortuga
That is exactly what the regex does. It keeps the linebreaks and discards the indentations. If you change the code console.log(a); to console.log(JSON.stringify(a));, you can more clearly see what the code outputs.Tallis
But I just tried your code, it removes all the line-breaks as well. I want to compose an email content with paragraphs. I need an empty line between paragraphs to make it more readable.Tortuga
@AntonioOoi If you want to keep the empty lines, use [^\S\r\n] instead \sDesimone
@AntonioOoi This can be done purely on compile-time. I created a Babel plugin that does not require putting any tag (function name) before the template literal, preserves newlines but gets rid of the leading whitespace at compile-time. It preserves empty lines as well. Maybe it would better suit your needs? More details are on my answer.Hardball
H
4

The problem with all the existing answers is, they are run-time solutions. That is, they take the multi-line template literal and run it through a function, while the program is executing, to get rid of the leading whitespace. This is "The Wrong Way" of doing it, because this operation is supposed to be done at compile-time. The reason is, there is nothing in this operation that requires run-time information, all information needed for this is known at compile-time.

To do it at compile-time, I wrote a Babel plugin named Dedent Template Literals. Basically it works as follows:

const httpRFC = `                Hypertext Transfer Protocol -- HTTP/1.1

                 Status of this Memo

                    This document specifies an Internet standards track protocol for the
                    Internet community, and requests discussion and suggestions for
                    improvements.  Please refer to the current edition of the "Internet
                    Official Protocol Standards" (STD 1) for the standardization state
                    and status of this protocol.  Distribution of this memo is unlimited.

                 Copyright Notice

                    Copyright (C) The Internet Society (1999).  All Rights Reserved.`;

console.log(httpRFC);

Will print:

                Hypertext Transfer Protocol -- HTTP/1.1

Status of this Memo

   This document specifies an Internet standards track protocol for the
   Internet community, and requests discussion and suggestions for
   improvements.  Please refer to the current edition of the "Internet
   Official Protocol Standards" (STD 1) for the standardization state
   and status of this protocol.  Distribution of this memo is unlimited.

Copyright Notice

   Copyright (C) The Internet Society (1999).  All Rights Reserved.

Interpolations work without any problems as well. Also if you start a line before the first column after the opening backtick of the template literal, the plugin will throw an error, showing the location of the error. If the following file is at src/httpRFC.js under your project:

const httpRFC = `                Hypertext Transfer Protocol -- HTTP/1.1

                 Status of this Memo

                    This document specifies an Internet standards track protocol for the
                    Internet community, and requests discussion and suggestions for
                    improvements.  Please refer to the current edition of the "Internet
                    Official Protocol Standards" (STD 1) for the standardization state
                    and status of this protocol.  Distribution of this memo is unlimited.

                Copyright Notice

                    Copyright (C) The Internet Society (1999).  All Rights Reserved.`;

console.log(httpRFC);

The following error will occur while transpiling:

Error: <path to your project>/src/httpRFC.js: LINE: 11, COLUMN: 17. Line must start at least at column 18.
    at PluginPass.dedentTemplateLiteral (<path to your project>/node_modules/babel-plugin-dedent-template-literals/index.js:39:15)
    at newFn (<path to your project>/node_modules/@babel/traverse/lib/visitors.js:175:21)
    at NodePath._call (<path to your project>/node_modules/@babel/traverse/lib/path/context.js:55:20)
    at NodePath.call (<path to your project>/node_modules/@babel/traverse/lib/path/context.js:42:17)
    at NodePath.visit (<path to your project>/node_modules/@babel/traverse/lib/path/context.js:92:31)
    at TraversalContext.visitQueue (<path to your project>/node_modules/@babel/traverse/lib/context.js:116:16)
    at TraversalContext.visitSingle (<path to your project>/node_modules/@babel/traverse/lib/context.js:85:19)
    at TraversalContext.visit (<path to your project>/node_modules/@babel/traverse/lib/context.js:144:19)
    at Function.traverse.node (<path to your project>/node_modules/@babel/traverse/lib/index.js:82:17)
    at NodePath.visit (<path to your project>/node_modules/@babel/traverse/lib/path/context.js:99:18) {
  code: 'BABEL_TRANSFORM_ERROR'
}

It also works with tabs if you are using tabs (and only tabs) for indentation and using spaces (and only spaces) for alignment.

It can be installed by running npm install --save-dev babel-plugin-dedent-template-literals and used by putting dedent-template-literals as the first element of the plugins array in Babel configuration. Further information can be found on the README.

Hardball answered 5/2, 2021 at 0:57 Comment(0)
B
3
function trim(segments, ...args) {
    const lines = segments
        .reduce((acc, segment, i) =>  acc + segment + (args[i] || ''), '') // get raw string
        .trimEnd().split('\n') // Split the raw string into lines
        .filter(line => line != "") // remove empty lines

    // Find the minimum number of leading spaces across all lines
    const minLeadingSpaces = lines.reduce((acc, line) => {
        // Find the number of leading spaces for this line
        const leadingSpaces = line.match(/^ */)[0].length
        // if it has less leading spaces than the previous minimum, set it as the new minimum
        return leadingSpaces < acc ? leadingSpaces :  acc
    }, Infinity)

    // Trim lines, join them and return the result
    return lines.map(line => line.substring(minLeadingSpaces)).join('\n')
}

Usage:

console.log(trim`
  <div>
    <p>
      Lorem ipsum dolor sit amet.
    </p>
  </div>
`)
Bartolemo answered 7/1, 2023 at 16:56 Comment(1)
thanks, the only decent answer hereStall
R
1

As Šime Vidas states, functions can be used as a tag, and invoked by just placing it in front of the template string.

A number of NPM modules exist to do this, and will cover many edge cases that would be hard to cover yourself. The main two are:

dedent, 4 million weekly downloads, last updated 4 years ago
dedent npm badge, with the above statistics

endent, 2.5 thousand weekly downloads, updated 4 months ago
endent npm badge, with the above statistics

Rachealrachel answered 10/8, 2020 at 10:21 Comment(0)
D
0

If you don't want to use a library and don't want to loose all "indentation structure" you can use this function:

function dedent(str) {
    const smallestIndent = Math.min(...str
        .split('\n')
        .filter(line => line.trim())
        .map(line => line.match(/^\s*/)[0].length)
    );
    return str
        .split('\n')
        .map(line => line.slice(smallestIndent))
        .join('\n');
}

The function figures out the smallest indent every line has in common, an then strips this amount of characters from each line.

This converts this:

        <head>
            <link rel='stylesheet' href='${server[source]}/widget.css'>
        </head>
        <body>
            ${widgetContainerText}
            <script type='module' src='${server[source]}/widget.js'>
        </body>

to this:

<head>
    <link rel='stylesheet' href='${server[source]}/widget.css'>
</head>
<body>
    ${widgetContainerText}
    <script type='module' src='${server[source]}/widget.js'>
</body>
Displeasure answered 28/3 at 13:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.