Last active
February 2, 2024 09:48
-
-
Save nberlette/664884d3e9cf67fbc14ad67428e75fa7 to your computer and use it in GitHub Desktop.
`Outdent`: remove common indentation in TypeScript (on type-level and value-level)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (c) 2024+ Nicholas Berlette (https://github.com/nberlette) | |
// Published under the MIT license. All rights reserved. | |
/** | |
* Removes common leading whitespace from each line in {@linkcode S}. | |
* This is the type-level counterpart to the {@linkcode outdent} function. | |
* | |
* @example | |
* ```ts | |
* type S = Outdent<` | |
* const foo = 1; | |
* if (foo) { | |
* if (foo > 1) { | |
* console.log(foo); | |
* } | |
* } | |
* `>; | |
* // type S = "const foo = 1;\nif (foo) {\n if (foo > 1) {\n console.log(foo);\n }\n}\n" | |
* ``` | |
*/ | |
export type Outdent<S extends string> = | |
| OutdentHelper<S> extends infer R extends string ? R : string; | |
/** | |
* Removes common leading whitespace from each line in the template string, | |
* while respecting indentation levels beyond the shared whitespace. | |
* | |
* @example | |
* ```ts | |
* const s = outdent(` | |
* const foo = 1; | |
* if (foo) { | |
* if (foo > 1) { | |
* console.log(foo); | |
* } | |
* } | |
* `); | |
* console.log(s); | |
* | |
* // Output: | |
* const foo = 1; | |
* if (foo) { | |
* if (foo > 1) { | |
* console.log(foo); | |
* } | |
* } | |
* ``` | |
*/ | |
export function outdent<S extends string>(string: S): Outdent<S>; | |
/** | |
* Removes common leading whitespace from each line in the template string, | |
* while respecting indentation levels beyond the shared whitespace. | |
*/ | |
export function outdent<const V extends readonly unknown[]>( | |
strings: TemplateStringsArray, | |
...values: V | |
): string; | |
export function outdent(string: string): string; | |
export function outdent( | |
str: string | TemplateStringsArray, | |
...values: unknown[] | |
): string { | |
if (typeof str !== "string" && str != null && "raw" in str) { | |
str = String.raw({ raw: str }, ...values) | |
} | |
str = String(str).normalize("NFKC"); | |
const whitespaceRe = new RegExp(`^[${whitespace.join("")}]+`, "gum"); | |
// normalize weird whitespace | |
str = str.replace(whitespaceRe, " "); | |
// normalize line endings | |
str = str.replace(/\r\n|\r|\n/g, "\n"); | |
// remove excessive newlines, but keep double newlines | |
str = str.replace(/\n{3,}/g, "\n\n"); | |
// remove trailing whitespace | |
str = str.replace(/[ \t]+$/gm, ""); | |
// normalize indentation to spaces (disabled for now) | |
//str = str.replace(/\t/g, " ".repeat(4)); | |
const lines = str.split(/\n/); | |
const minIndent = lines.reduce( | |
(min, line) => Math.min(Math.max(min, 0), line.search(/\S/)), | |
Number.MAX_SAFE_INTEGER, | |
); | |
return lines.reduce( | |
(str, line) => `${str}${line.replace(new RegExp(`^[ ]{${minIndent}}`), "").trimEnd()}\n`, | |
"" | |
); | |
} | |
// #region Internals | |
const whitespaceLike = [ | |
"\u{B}", // vertical tab | |
"\u{C}", // form feed | |
"\u{20}", // space | |
"\u{A0}", | |
"\u{2000}", | |
"\u{2001}", | |
"\u{2002}", | |
"\u{2003}", | |
"\u{2004}", | |
"\u{2005}", | |
"\u{2006}", | |
"\u{2007}", | |
"\u{2008}", | |
"\u{2009}", | |
"\u{200A}", | |
"\u{202F}", | |
"\u{205F}", | |
"\u{3000}", | |
] as const; | |
const lineTerminators = [ | |
"\u{A}", | |
"\u{D}", | |
"\u{2028}", | |
"\u{2029}", | |
] as const; | |
const whitespace = [ | |
...whitespaceLike, | |
...lineTerminators, | |
"\u{9}", // tab | |
"\u{FEFF}", // zero-width no-break space | |
] as const; | |
type Whitespace = "\t" | typeof whitespace[number]; | |
type LineTerminators = "\u{A}" | "\u{D}" | "\u{2028}" | "\u{2029}"; | |
type TrimLeft< | |
S extends string, | |
N extends number = 1, | |
A extends readonly 0[] = [], | |
> = [N] extends [A["length"]] ? S | |
: S extends `${Whitespace}${infer R}` ? TrimLeft<R, N, [...A, 0]> | |
: S; | |
type MeasureIndentation<S extends string, A extends readonly 0[] = []> = | |
| S extends `${infer L}${infer R}` | |
? L extends `${Whitespace}${infer R2}` | |
? MeasureIndentation<R, [...A, 0]> | |
: A["length"] | |
: 0; | |
type OutdentHelper<S extends string, N extends number = MeasureIndentation<S>> = | |
| string extends S ? string | |
: S extends "" ? "" | |
: S extends `${LineTerminators}${infer R}` ? OutdentHelper<R> | |
: S extends `${infer L}\n${infer R}` | |
? L extends `${Whitespace}${string}` | |
? MeasureIndentation<L> extends N | |
? `${TrimLeft<L, N>}\n${OutdentHelper<R, N>}` | |
: `${TrimLeft<L, N>}\n${OutdentHelper<R, N>}` | |
: S | |
: S extends `${Whitespace}${string}` ? `${TrimLeft<S, N>}` | |
: S; | |
// #endregion Internal |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment