TODO: Add a description of the project here.
import { outdent } from "https://gist.githubusercontent.com/nberlette";
export 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; | |
export const lineTerminators = [ | |
"\u{A}", | |
"\u{D}", | |
"\u{2028}", | |
"\u{2029}", | |
] as const; | |
export const whitespace = [ | |
...whitespaceLike, | |
...lineTerminators, | |
"\u{9}", // tab | |
"\u{FEFF}", // zero-width no-break space | |
] as const; | |
export type whitespace = typeof whitespace; | |
export type Whitespace = whitespace[number]; | |
export type LineTerminators = "\u{A}" | "\u{D}" | "\u{2028}" | "\u{2029}"; | |
export 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; | |
export 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; | |
export 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; |
The MIT License (MIT) | |
Copyright © 2023-2024 Nicholas Berlette (https://github.com/nberlette) | |
Permission is hereby granted, free of charge, to any person obtaining a copy of | |
this software and associated documentation files (the “Software”), to deal in | |
the Software without restriction, including without limitation the rights to | |
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | |
the Software, and to permit persons to whom the Software is furnished to do so, | |
subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | |
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | |
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | |
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | |
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
export { outdent, outdent as default } from "./outdent.ts"; | |
export type { Outdent, OutdentOptions } from "./types.ts"; |
/*! MIT. Copyright (c) 2024+ Nicholas Berlette. All rights reserved. */ | |
import type { Outdent, OutdentOptions } from "./types.ts"; | |
import { whitespaceLike } from "./_internal.ts"; | |
type TemplateValue<T> = T | OutdentOptions | typeof outdent; | |
/** | |
* 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 T, const V extends readonly TemplateValue<T>[]>( | |
strings: TemplateStringsArray, | |
...values: [...V] | |
): string; | |
export function outdent(string: string, options?: OutdentOptions): string; | |
export function outdent<T>( | |
template: TemplateStringsArray, | |
...values: TemplateValue<T>[] | |
): string; | |
export function outdent<T>( | |
template: TemplateStringsArray | string, | |
...values: TemplateValue<T>[] | |
): string { | |
const options: OutdentOptions = {}; | |
let str = typeof template === "string" ? template : ""; | |
if (typeof template === "string") { | |
Object.assign(options, values[0] ?? {}); | |
} else { | |
const p = values.map((value, i) => { | |
if (value && typeof value === "object") { | |
Object.assign(options, value); | |
return ""; | |
} else if (typeof value === "function") { | |
if (value === outdent) { | |
options.spaces = parseInt( | |
String(template[i].match(/(\s*)$/)?.[1] ?? options.spaces ?? 0), | |
); | |
} | |
return ""; | |
} | |
return value; | |
}); | |
str = template.raw.reduce((a, s, i) => `${a}${s}${p[i] ?? ""}`, ""); | |
} | |
const { | |
useTabs = false, | |
tabWidth = useTabs ? 2 : undefined!, | |
trimStart = false, | |
trimEnd = false, | |
lineWidth = 80, | |
preserveTabsAndSpaces = true, | |
normalizeEndings = true, | |
normalizeWhitespace = !preserveTabsAndSpaces, | |
removeEmptyLines = 2, | |
spaces = useTabs ? undefined! : 2, | |
eol = "\n", | |
wordwrap = false, | |
} = options; | |
const MAX_INDENT_LENGTH = 100; | |
const LEADING_TAB_RE = /(?<=^|\s)(\t)/g; | |
const LEADING_SPACE_RE = new RegExp(`(?<=^|\\s)([ ]{${tabWidth}})`, "g"); | |
const WORDWRAP_RE = new RegExp( | |
`^(.{1,${lineWidth}}(?=(?:(?![\\r\\n])\\s+)|$\\n?))|(.{${lineWidth}}\\b)`, | |
"gum", | |
); | |
const WHITESPACE_RE = new RegExp(`^([${whitespaceLike.join("")}]+)`, "gum"); | |
// deno-lint-ignore no-control-regex | |
const LINE_ENDINGS_RE = /\r\n|\r|\u2028|\u2029|\u000B|\u000C/g; | |
const EOL_RE = new RegExp(`(${eol}){3,}`, "g"); | |
// normalize weird whitespace | |
if (normalizeWhitespace) { | |
str = str.replace(WHITESPACE_RE, (m) => " ".repeat(m.length)); | |
} | |
if (normalizeEndings) { | |
str = str.replace(LINE_ENDINGS_RE, eol); | |
} | |
if (!preserveTabsAndSpaces && !useTabs && spaces === undefined) { | |
str = str.replace(/\t/g, " ".repeat(tabWidth)); | |
} | |
if (removeEmptyLines === true) { | |
str = str.replace(EOL_RE, `${eol}${eol}`); | |
} | |
let lines = str.split(eol); | |
if (lines.length) { | |
if (trimStart) lines.unshift(lines.shift()?.trimStart()!); | |
if (trimEnd) lines[lines.length - 1] = lines.at(-1)?.trimEnd()!; | |
const tabs = spaces === undefined && useTabs; | |
const search = tabs ? LEADING_SPACE_RE : LEADING_TAB_RE; | |
const replace = tabs ? "\t" : " ".repeat(tabWidth); | |
lines = lines.map((line) => line.replace(search, replace)); | |
const minIndent = lines.reduce( | |
(min, line) => { | |
if (/^\s*$/.test(line)) return min; | |
const lineIndent = line.match(/^\s*/)?.[0].length ?? 0; | |
return Math.min(min, lineIndent); | |
}, | |
Infinity, | |
); | |
// match all leading spaces/tabs at the start of each line | |
const match = str.match(/^[ \t]*(?=\S)/gm); | |
// find the smallest indent, we don't want to remove all leading whitespace | |
const indent = Math.min( | |
MAX_INDENT_LENGTH, | |
...(match ?? [""])?.map((el) => el.length), | |
); | |
const INDENT_RE = new RegExp( | |
`^[ \\t]{${Math.min(minIndent, indent)}}`, | |
"gm", | |
); | |
// const indentLevel = spaces ?? minIndent; | |
// const tabLevel = Math.ceil(indentLevel / tabWidth); | |
lines = lines.map((line) => { | |
// const level = tabs ? tabLevel : indentLevel; | |
// const prefix = line.slice(0, level); | |
if (line.search(/\S/) < 1) return line; | |
return line.replace(INDENT_RE, ""); | |
}); | |
if (lineWidth > 0 && wordwrap !== false) { | |
if (wordwrap === "hard") { | |
lines = lines.flatMap((line, i, arr) => { | |
if (line.length <= lineWidth) return line; | |
const wrapped = line.slice(0, lineWidth); | |
const leftover = line.slice(wrapped.length).trimEnd(); | |
let next = arr[i + 1] ?? ""; | |
if (leftover.length) { | |
const nextIndent = next.match(/^\s*/)?.[0] ?? ""; | |
arr[i + 1] = next = nextIndent + leftover + | |
next.slice(nextIndent.length); | |
} | |
return wrapped; | |
}); | |
} else { | |
lines = lines.map((line) => | |
line.replace(WORDWRAP_RE, (m, p1, p2) => { | |
let str = p1 ?? p2 ?? m; | |
str = str.replace(/$\n?|\s+$/m, (m) => m.length > 1 ? eol : ""); | |
if (str.length <= lineWidth) return str; | |
const parts = str.split(/(\s+)/); | |
let result = "", line = ""; | |
for (const part of parts) { | |
if ((line + part).length > lineWidth) { | |
result += line + eol; | |
line = ""; | |
} else if (line.length > lineWidth) { | |
result += line + eol; | |
line = part; | |
} else if (part.trim().length || line.length) { | |
line += part; | |
} else if (result.length) { | |
result += part; | |
} else { | |
line += part; | |
} | |
} | |
return result + line; | |
}) | |
); | |
} | |
} | |
} | |
return lines.join(eol); | |
} |
import type { OutdentHelper } from "./_internal.ts"; | |
/** | |
* 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; | |
/** Options for the `outdent` module. */ | |
export interface OutdentOptions { | |
/** The character to use for the end of line. Default is `"\n"`. */ | |
eol?: string; | |
/** | |
* Normalizes irregular line endings, replacing several different characters | |
* that are often used for the same purpose. The replacement string depends | |
* on the {@linkcode eol} option, which is a line feed (`U+000A`) by default. | |
* | |
* The following characters are replaced: | |
* - `U+000D` (carriage return) | |
* - `U+2028` (line separator) | |
* - `U+2029` (paragraph separator) | |
* - `U+000D` + `U+000A` (carriage return + line feed) | |
* - `U+000B` (vertical tab) | |
* - `U+000C` (form feed) | |
*/ | |
normalizeEndings?: boolean; | |
/** Normalizes irregular whitespace to a standard space character (`U+0020`). | |
* | |
* The following characters are replaced: | |
* - `U+000B` (vertical tab) | |
* - `U+000C` (form feed) | |
* - `U+0020` (space) | |
* - `U+00A0` (no-break space) | |
* - `U+2000` (en quad) | |
* - `U+2001` (em quad) | |
* - `U+2002` (en space) | |
* - `U+2003` (em space) | |
* - `U+2004` (three-per-em space) | |
* - `U+2005` (four-per-em space) | |
* - `U+2006` (six-per-em space) | |
* - `U+2007` (figure space) | |
* - `U+2008` (punctuation space) | |
* - `U+2009` (thin space) | |
* - `U+200A` (hair space) | |
* - `U+202F` (narrow no-break space) | |
* - `U+205F` (medium mathematical space) | |
* - `U+3000` (ideographic space) | |
* - `U+FEFF` (zero-width no-break space) | |
*/ | |
normalizeWhitespace?: boolean; | |
/** If `true`, mixed tabs and spaces are preserved as-is, and {@link tabWidth} | |
* will be used to attempt to outdent the text while preserving the existing | |
* indentation characters. This option is experimental, and may produce mixed | |
* results. It is ignored if {@link useTabs} or {@link spaces} are set. */ | |
preserveTabsAndSpaces?: boolean; | |
/** If `true`, anything beyond two consecutive empty lines will be reduced to | |
* two empty lines. If a number is provided, it will be used as the maximum | |
* number of consecutive empty lines to allow instead of two. An empty line | |
* is defined as a line that contains only whitespace characters, matching | |
* the following regular expression: `/^\s*$/`. */ | |
removeEmptyLines?: boolean | number; | |
/** The number of spaces to use for indentation. If not provided, the value | |
* will be determined by the minimum amount of leading whitespace that is | |
* common to all non-empty lines. When using the tagged template literal | |
* syntax, you may also provide a reference to the `outdent` function itself | |
* as a placeholder value, to mark the position where the indentation level | |
* should be determined. | |
* | |
* @example | |
* ```ts | |
* const result = outdent` | |
* ${outdent} // <- use the outdent function to mark the indent level (2) | |
* if (true) { | |
* console.log('Hello, world!'); | |
* } | |
* `; | |
* | |
* console.log(result); | |
* // Output: | |
* // if (true) { | |
* // console.log('Hello, world!'); | |
* // } | |
* ``` | |
*/ | |
spaces?: number; | |
/** | |
* If `true`, leading whitespace is removed from the start of the string. | |
* Otherwise it will be preserved. | |
* | |
* Default is `false`. */ | |
trimStart?: boolean; | |
/** | |
* If `true`, trailing whitespace is removed from the end of the string. | |
* Otherwise, it will be preserved. | |
* | |
* Default is `false`. */ | |
trimEnd?: boolean; | |
/** The width of a tab character, in number of spaces. Default is `2`. */ | |
tabWidth?: number; | |
/** | |
* If `true`, tabs are used for indentation rather than spaces. If `false`, | |
* spaces will be used instead. | |
* | |
* Default is `false`. */ | |
useTabs?: boolean; | |
/** | |
* The maximum line width to use for word wrapping. The is only used if the | |
* {@link wordwrap} option is set to `true`, `"hard"`, or `"soft"`. | |
* | |
* Default is `80`. */ | |
lineWidth?: number; | |
/** | |
* UNSTABLE: Enable/disable/control word-wrapping behavior. | |
* | |
* If `true`, the text will be wrapped to fit within the {@link lineWidth} | |
* limit. If `"hard"`, the text will be wrapped at the breakpoint that is | |
* closest to the exact width, and whitespace will not be preserved. If | |
* `"soft"`, the text will be wrapped at the nearest whitespace character, | |
* while respecting existing whitespace and line terminators. | |
* | |
* Default is `false`. */ | |
wordwrap?: boolean | "hard" | "soft"; | |
} |