1 line
12 KiB
Text
1 line
12 KiB
Text
|
|
{"version":3,"file":"truncate.cjs","names":[],"sources":["../src/truncate.ts"],"sourcesContent":["import type {\n And,\n IsEqual,\n IsNever,\n IsStringLiteral,\n NonNegativeInteger,\n} from \"type-fest\";\nimport type { ClampedIntegerSubtract } from \"./internal/types/ClampedIntegerSubtract\";\nimport type { StringLength } from \"./internal/types/StringLength\";\n\ntype TruncateOptions = {\n readonly omission?: string;\n readonly separator?: string | RegExp;\n};\n\nconst DEFAULT_OMISSION = \"...\";\n\ntype Truncate<\n S extends string,\n N extends number,\n Options extends TruncateOptions,\n> =\n IsNever<NonNegativeInteger<N>> extends true\n ? // Exit early when N isn't a literal non-negative integer.\n string\n : TruncateWithOptions<\n S,\n N,\n // TODO: I don't like how I handled the default options object; I want to have everything coupled between the runtime and the type system, but this feels both brittle to changes, and over-verbose.\n Options extends Pick<Required<TruncateOptions>, \"omission\">\n ? Options[\"omission\"]\n : typeof DEFAULT_OMISSION,\n Options extends Pick<Required<TruncateOptions>, \"separator\">\n ? Options[\"separator\"]\n : undefined\n >;\n\ntype TruncateWithOptions<\n S extends string,\n N extends number,\n Omission extends string,\n Separator extends string | RegExp | undefined,\n> =\n // Distribute the result over unions.\n N extends unknown\n ? // We can short-circuit most of our logic when N is a literal 0.\n IsEqual<N, 0> extends true\n ? \"\"\n : // Distribute the result over unions.\n Omission extends unknown\n ? // When Omission isn't literal we don't know how long it is.\n IsStringLiteral<Omission> extends true\n ? // This mirrors the runtime logic where if `n - omission.length`\n // is not positive then what we end up truncating is Omission\n // itself and not S.\n IsEqual<\n ClampedIntegerSubtract<N, StringLength<Omission>>,\n 0\n > extends true\n ? TruncateLiterals<Omission, N, \"\">\n : And<\n // When S isn't literal the output wouldn't be literal\n // either.\n IsStringLiteral<S>,\n // TODO: Handling non-trivial separators would add a ton of complexity to this type! It's possible (but hard!) to support string literals so I'm leaving this as a TODO; regular expressions are impossible because we can't get the type checker to run them.\n IsEqual<Separator, undefined>\n > extends true\n ? TruncateLiterals<S, N, Omission>\n : string\n : string\n : never\n : never;\n\n/**\n * This is the actual implementation of the truncation logic. It assumes all\n * its params are literals and valid.\n */\ntype TruncateLiterals<\n S extends string,\n N extends number,\n Omission extends string,\n Iteration extends readonly unknown[] = [],\n> = S extends `${infer Character}${infer Rest}`\n ? // The cutoff point N - omission.length leaves room for the omission.\n Iteration[\"length\"] extends ClampedIntegerSubtract<\n N,\n StringLength<Omission>\n >\n ? // The string is only truncated if its total length is longer than N; at\n // the cutoff point this is simplified to comparing the remaining suffix\n // length to the omission length.\n IsLongerThan<S, Omission> extends true\n ? Omission\n : S\n : // Reconstruct string character by character until cutoff.\n `${Character}${TruncateLiterals<Rest, N, Omission, [...Iteration, unknown]>}`\n : // Empty input string results in empty output.\n \"\";\n\n/**\n * An optimized check that efficiently checks if the string A is longer than B.\n */\ntype IsLongerThan<\n A extends string,\n B extends string,\n> = A extends `${string}${infer RestA}`\n ? B extends `${string}${infer RestB}`\n ? IsLongerThan<RestA, RestB>\
|