Skip to content

Instantly share code, notes, and snippets.

@nberlette
Last active February 2, 2024 09:48
Show Gist options
  • Save nberlette/664884d3e9cf67fbc14ad67428e75fa7 to your computer and use it in GitHub Desktop.
Save nberlette/664884d3e9cf67fbc14ad67428e75fa7 to your computer and use it in GitHub Desktop.
`Outdent`: remove common indentation in TypeScript (on type-level and value-level)
// 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