index.js 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. import stripAnsi from 'strip-ansi';
  2. import {eastAsianWidth} from 'get-east-asian-width';
  3. import emojiRegex from 'emoji-regex';
  4. const segmenter = new Intl.Segmenter();
  5. const defaultIgnorableCodePointRegex = /^\p{Default_Ignorable_Code_Point}$/u;
  6. export default function stringWidth(string, options = {}) {
  7. if (typeof string !== 'string' || string.length === 0) {
  8. return 0;
  9. }
  10. const {
  11. ambiguousIsNarrow = true,
  12. countAnsiEscapeCodes = false,
  13. } = options;
  14. if (!countAnsiEscapeCodes) {
  15. string = stripAnsi(string);
  16. }
  17. if (string.length === 0) {
  18. return 0;
  19. }
  20. let width = 0;
  21. const eastAsianWidthOptions = {ambiguousAsWide: !ambiguousIsNarrow};
  22. for (const {segment: character} of segmenter.segment(string)) {
  23. const codePoint = character.codePointAt(0);
  24. // Ignore control characters
  25. if (codePoint <= 0x1F || (codePoint >= 0x7F && codePoint <= 0x9F)) {
  26. continue;
  27. }
  28. // Ignore zero-width characters
  29. if (
  30. (codePoint >= 0x20_0B && codePoint <= 0x20_0F) // Zero-width space, non-joiner, joiner, left-to-right mark, right-to-left mark
  31. || codePoint === 0xFE_FF // Zero-width no-break space
  32. ) {
  33. continue;
  34. }
  35. // Ignore combining characters
  36. if (
  37. (codePoint >= 0x3_00 && codePoint <= 0x3_6F) // Combining diacritical marks
  38. || (codePoint >= 0x1A_B0 && codePoint <= 0x1A_FF) // Combining diacritical marks extended
  39. || (codePoint >= 0x1D_C0 && codePoint <= 0x1D_FF) // Combining diacritical marks supplement
  40. || (codePoint >= 0x20_D0 && codePoint <= 0x20_FF) // Combining diacritical marks for symbols
  41. || (codePoint >= 0xFE_20 && codePoint <= 0xFE_2F) // Combining half marks
  42. ) {
  43. continue;
  44. }
  45. // Ignore surrogate pairs
  46. if (codePoint >= 0xD8_00 && codePoint <= 0xDF_FF) {
  47. continue;
  48. }
  49. // Ignore variation selectors
  50. if (codePoint >= 0xFE_00 && codePoint <= 0xFE_0F) {
  51. continue;
  52. }
  53. // This covers some of the above cases, but we still keep them for performance reasons.
  54. if (defaultIgnorableCodePointRegex.test(character)) {
  55. continue;
  56. }
  57. // TODO: Use `/\p{RGI_Emoji}/v` when targeting Node.js 20.
  58. if (emojiRegex().test(character)) {
  59. width += 2;
  60. continue;
  61. }
  62. width += eastAsianWidth(codePoint, eastAsianWidthOptions);
  63. }
  64. return width;
  65. }