| 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 | */ |
| 6 | export function dedent( |
| 7 | strings: TemplateStringsArray, |
| 8 | ...values: unknown[] |
| 9 | ): string; |
| 10 | export function dedent(text: string): string; |
| 11 | export 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 | } |