1/**
2 * Tagged template literal for removing indentation from a block of text.
3 *
4 * If the first line is empty, it will be ignored.
5 */
6export function dedent(
7 strings: TemplateStringsArray,
8 ...values: unknown[]
9): string;
10export function dedent(text: string): string;
11export function dedent(
12 stringsOrText: TemplateStringsArray | string,
13 ...values: unknown[]
14): string {
15 const raw =
16 typeof stringsOrText === "string"
17 ? stringsOrText
18 : String.raw({ raw: stringsOrText }, ...values);
19
20 let lines = raw.split("\n");
21
22 while (lines.length > 0 && lines[0].trim() === "") {
23 lines = lines.slice(1);
24 }
25
26 while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
27 lines = lines.slice(0, lines.length - 1);
28 }
29
30 if (lines.length === 0) {
31 return "";
32 }
33
34 let minIndentLength = Number.POSITIVE_INFINITY;
35 for (const line of lines) {
36 if (line.trim() !== "") {
37 const indent = line.match(/^[ \t]*/)?.[0];
38 if (indent != null && indent.length < minIndentLength) {
39 minIndentLength = indent.length;
40 }
41 }
42 }
43
44 if (minIndentLength === Number.POSITIVE_INFINITY) {
45 return lines.join("\n");
46 }
47
48 lines = lines.map((line) => {
49 if (line.trim() === "") {
50 return line;
51 }
52 return line.startsWith(" ".repeat(minIndentLength)) ||
53 line.startsWith("\t".repeat(minIndentLength))
54 ? line.substring(minIndentLength)
55 : line;
56 });
57
58 return lines.join("\n");
59}