middleEllipsis.tsx 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. /**
  2. * Trim strings with a preference for preserving whole words. Only cut up
  3. * whole words if the last remaining words are still too long.
  4. *
  5. * @param value The string to trim
  6. * @param maxLength The maximum length of the string
  7. * @param delimiter The delimiter to split the string by. If passing a regex be aware that the algorithm only supports single-character delimiters.
  8. *
  9. * **Examples:**
  10. *
  11. * - javascript project backend -> javascript…backend
  12. * - my long sentry project name -> my long…project name
  13. * - javascriptproject backend -> javascriptproj…ackend
  14. */
  15. export function middleEllipsis(
  16. value: string,
  17. maxLength: number,
  18. delimiter: string | RegExp = ' '
  19. ) {
  20. // Return the original slug if it's already shorter than maxLength
  21. if (value.length <= maxLength) {
  22. return value;
  23. }
  24. /**
  25. * Array of words inside the string.
  26. * E.g. "my project name" becomes ["my", "project", "name"]
  27. */
  28. const words: string[] = value.split(delimiter);
  29. const delimiters = Array.from(value.match(new RegExp(delimiter, 'g')) || []);
  30. // If the string is too long but not hyphenated, return an end-trimmed
  31. // string. E.g. "javascriptfrontendproject" --> "javascriptfrontendp…"
  32. if (words.length === 1) {
  33. return `${value.slice(0, maxLength - 1)}\u2026`;
  34. }
  35. /**
  36. * Returns the length (total number of letters plus hyphens in between
  37. * words) of the current words array.
  38. */
  39. function getLength(arr: string[]): number {
  40. return arr.reduce((acc, cur) => acc + cur.length + 1, 0) - 1;
  41. }
  42. // Progressively remove words and delimiters in the middle until we're below maxLength,
  43. // or when only two words are left
  44. while (getLength(words) > maxLength && words.length > 2) {
  45. words.splice(Math.floor(words.length / 2 - 0.5), 1);
  46. }
  47. // If the remaining words array satisfies the maxLength requirement,
  48. // return the trimmed result.
  49. if (getLength(words) <= maxLength) {
  50. const divider = Math.floor(words.length / 2);
  51. const firstHalf = words.slice(0, divider);
  52. const firstHalfWithDelimiters = firstHalf.flatMap((word, i) =>
  53. i === divider - 1 ? [word] : [word, delimiters[i]]
  54. );
  55. const secondHalf = words.slice(divider);
  56. const secondHalfWithDelimiters = secondHalf.flatMap((word, i) =>
  57. i === 0 ? [word] : [delimiters[delimiters.length - secondHalf.length + i], word]
  58. );
  59. return `${firstHalfWithDelimiters.join('')}\u2026${secondHalfWithDelimiters.join(
  60. ''
  61. )}`;
  62. }
  63. // If the remaining 2 words are still too long, trim those words starting
  64. // from the middle.
  65. const debt = getLength(words) - maxLength;
  66. const toTrimFromLeftWord = Math.ceil(debt / 2);
  67. const leftWordLength = Math.max(words[0].length - toTrimFromLeftWord, 3);
  68. const leftWord = words[0].slice(0, leftWordLength);
  69. const rightWordLength = maxLength - leftWord.length;
  70. const rightWord = words[1].slice(-rightWordLength);
  71. return `${leftWord}\u2026${rightWord}`;
  72. }