UNPKG

remeda

Version:

A utility library for JavaScript and Typescript.

1 lines 12.2 kB
{"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 ReadonlyArray<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>\n : // B is empty and A isn't!\n true\n : // A is empty, even if B is empty, A wouldn't be (strictly) longer.\n false;\n\n/**\n * Truncates strings to a maximum length, adding an ellipsis when truncated.\n *\n * Shorter strings are returned unchanged. If the omission marker is longer than\n * the maximum length, it will be truncated as well.\n *\n * The `separator` argument provides more control by optimistically searching\n * for a matching cutoff point, which could be used to avoid truncating in the\n * middle of a word or other semantic boundary.\n *\n * If you just need to limit the total length of the string, without adding an\n * `omission` or optimizing the cutoff point via `separator`, prefer\n * `sliceString` instead, which runs more efficiently.\n *\n * The function counts Unicode characters, not visual graphemes, and may split\n * emojis, denormalized diacritics, or combining characters, in the middle. For\n * display purposes, prefer CSS [`text-overflow: ellipsis`](https://developer.mozilla.org/en-US/docs/Web/CSS/text-overflow#ellipsis)\n * which is locale-aware and purpose-built for this task.\n *\n * @param data - The input string.\n * @param n - The maximum length of the output string. The output will **never**\n * exceed this length.\n * @param options - An optional options object.\n * @param options.omission - The string that is appended to the end of the\n * output *whenever the input string is truncated*. Default: '...'.\n * @param options.separator - A string or regular expression that defines a\n * cutoff point for the truncation. If multiple cutoff points are found, the one\n * closest to `n` will be used, and if no cutoff point is found then the\n * function will fallback to the trivial cutoff point. Regular expressions are\n * also supported. Default: <none> (which is equivalent to `\"\"` or the regular\n * expression `/./`).\n * @signature\n * R.truncate(data, n, { omission, separator });\n * @example\n * R.truncate(\"Hello, world!\", 8); //=> \"Hello...\"\n * R.truncate(\n * \"cat, dog, mouse\",\n * 12,\n * { omission: \"__\", separator: \",\"},\n * ); //=> \"cat, dog__\"\n * @dataFirst\n * @category String\n */\nexport function truncate<\n S extends string,\n N extends number,\n const Options extends TruncateOptions,\n>(data: S, n: N, options?: Options): Truncate<S, N, Options>;\n\n/**\n * Truncates strings to a maximum length, adding an ellipsis when truncated.\n *\n * Shorter strings are returned unchanged. If the omission marker is longer than\n * the maximum length, it will be truncated as well.\n *\n * The `separator` argument provides more control by optimistically searching\n * for a matching cutoff point, which could be used to avoid truncating in the\n * middle of a word or other semantic boundary.\n *\n * If you just need to limit the total length of the string, without adding an\n * `omission` or optimizing the cutoff point via `separator`, prefer\n * `sliceString` instead, which runs more efficiently.\n *\n * The function counts Unicode characters, not visual graphemes, and may split\n * emojis, denormalized diacritics, or combining characters, in the middle. For\n * display purposes, prefer CSS [`text-overflow: ellipsis`](https://developer.mozilla.org/en-US/docs/Web/CSS/text-overflow#ellipsis)\n * which is locale-aware and purpose-built for this task.\n *\n * @param n - The maximum length of the output string. The output will **never**\n * exceed this length.\n * @param options - An optional options object.\n * @param options.omission - The string that is appended to the end of the\n * output *whenever the input string is truncated*. Default: '...'.\n * @param options.separator - A string or regular expression that defines a\n * cutoff point for the truncation. If multiple cutoff points are found, the one\n * closest to `n` will be used, and if no cutoff point is found then the\n * function will fallback to the trivial cutoff point. Regular expressions are\n * also supported. Default: <none> (which is equivalent to `\"\"` or the regular\n * expression `/./`).\n * @signature\n * R.truncate(n, { omission, separator })(data);\n * @example\n * R.pipe(\"Hello, world!\" as const, R.truncate(8)); //=> \"Hello...\"\n * R.pipe(\n * \"cat, dog, mouse\" as const,\n * R.truncate(12, { omission: \"__\", separator: \",\"}),\n * ); //=> \"cat, dog__\"\n * @dataLast\n * @category String\n */\nexport function truncate<\n N extends number,\n const Options extends TruncateOptions,\n>(\n n: N,\n options?: Options,\n): <S extends string>(data: S) => Truncate<S, N, Options>;\n\nexport function truncate(\n dataOrN: string | number,\n nOrOptions?: number | TruncateOptions,\n options?: TruncateOptions,\n): unknown {\n return typeof dataOrN === \"string\"\n ? truncateImplementation(\n dataOrN,\n // @ts-expect-error [ts2345] -- We want to reduce runtime checks to a\n // minimum, and there's no (easy) way to couple params so that when we\n // check one, the others are inferred accordingly.\n nOrOptions,\n options,\n )\n : (data: string) =>\n truncateImplementation(\n data,\n dataOrN,\n // @ts-expect-error [ts2345] -- We want to reduce runtime checks to a\n // minimum, and there's no (easy) way to couple params so that when we\n // check one, the others are inferred accordingly.\n nOrOptions,\n );\n}\n\nfunction truncateImplementation(\n data: string,\n n: number,\n { omission = DEFAULT_OMISSION, separator }: TruncateOptions = {},\n): string {\n if (data.length <= n) {\n // No truncation needed.\n return data;\n }\n\n if (n <= 0) {\n // Avoid weirdness when n isn't positive.\n return \"\";\n }\n\n if (n < omission.length) {\n // TODO [>3]: This was an oversight, there's no value in returning just parts of the omission string itself, with no actual content. Instead, we should truncate the input without adding the omission at all in cases where the omission would completely eclipse the content.\n // Handle cases where the omission itself is too long.\n return omission.slice(0, n);\n }\n\n // Our trivial cutoff is the point where we can add the omission and reach\n // n exactly, this is what we'll use when no separator is provided.\n let cutoff = n - omission.length;\n\n if (typeof separator === \"string\") {\n const lastSeparator = data.lastIndexOf(separator, cutoff);\n if (lastSeparator !== -1) {\n // If we find the separator within the part of the string that would be\n // returned we move the cutoff further so that we also remove it.\n cutoff = lastSeparator;\n }\n } else if (separator !== undefined) {\n const globalSeparator = separator.flags.includes(\"g\")\n ? separator\n : new RegExp(separator.source, `${separator.flags}g`);\n\n let lastSeparator;\n for (const { index } of data.matchAll(globalSeparator)) {\n if (index > cutoff) {\n // We only care about separators within the part of the string that\n // would be returned anyway, once we are past that point we don't care\n // about any further separators.\n break;\n }\n lastSeparator = index;\n }\n if (lastSeparator !== undefined) {\n cutoff = lastSeparator;\n }\n }\n\n // Build the output.\n return `${data.slice(0, cutoff)}${omission}`;\n}\n"],"mappings":"AAeA,MAAM,EAAmB,MAoMzB,SAAgB,EACd,EACA,EACA,EACS,CACT,OAAO,OAAO,GAAY,SACtB,EACE,EAIA,EACA,EACD,CACA,GACC,EACE,EACA,EAIA,EACD,CAGT,SAAS,EACP,EACA,EACA,CAAE,WAAW,MAAkB,aAA+B,EAAE,CACxD,CACR,GAAI,EAAK,QAAU,EAEjB,OAAO,EAGT,GAAI,GAAK,EAEP,MAAO,GAGT,GAAI,EAAI,EAAS,OAGf,OAAO,EAAS,MAAM,EAAG,EAAE,CAK7B,IAAI,EAAS,EAAI,EAAS,OAE1B,GAAI,OAAO,GAAc,SAAU,CACjC,IAAM,EAAgB,EAAK,YAAY,EAAW,EAAO,CACrD,IAAkB,KAGpB,EAAS,WAEF,IAAc,IAAA,GAAW,CAClC,IAAM,EAAkB,EAAU,MAAM,SAAS,IAAI,CACjD,EACA,IAAI,OAAO,EAAU,OAAQ,GAAG,EAAU,MAAM,GAAG,CAEnD,EACJ,IAAK,GAAM,CAAE,WAAW,EAAK,SAAS,EAAgB,CAAE,CACtD,GAAI,EAAQ,EAIV,MAEF,EAAgB,EAEd,IAAkB,IAAA,KACpB,EAAS,GAKb,MAAO,GAAG,EAAK,MAAM,EAAG,EAAO,GAAG"}