diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d36e1a8..1ed55d5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,11 +10,12 @@ jobs: fail-fast: false matrix: node-version: - - 14 - - 12 + - 20 + - 18 + - 16 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/index.d.ts b/index.d.ts index aed9fdf..47b4e82 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,11 +1,11 @@ -export interface Options { +export type Options = { /** Count [ambiguous width characters](https://www.unicode.org/reports/tr11/#Ambiguous) as having narrow width (count of 1) instead of wide width (count of 2). @default true */ readonly ambiguousIsNarrow: boolean; -} +}; /** Get the visual width of a string - the number of columns required to display it. diff --git a/index.js b/index.js index 9294488..ddd1fdb 100644 --- a/index.js +++ b/index.js @@ -2,14 +2,23 @@ import stripAnsi from 'strip-ansi'; import eastAsianWidth from 'eastasianwidth'; import emojiRegex from 'emoji-regex'; -export default function stringWidth(string, options = {}) { +let segmenter; +function * splitString(string) { + segmenter ??= new Intl.Segmenter(); + + for (const {segment: character} of segmenter.segment(string)) { + yield character; + } +} + +export default function stringWidth(string, options) { if (typeof string !== 'string' || string.length === 0) { return 0; } options = { ambiguousIsNarrow: true, - ...options + ...options, }; string = stripAnsi(string); @@ -23,7 +32,7 @@ export default function stringWidth(string, options = {}) { const ambiguousCharacterWidth = options.ambiguousIsNarrow ? 1 : 2; let width = 0; - for (const character of string) { + for (const character of splitString(string)) { const codePoint = character.codePointAt(0); // Ignore control characters @@ -32,21 +41,26 @@ export default function stringWidth(string, options = {}) { } // Ignore combining characters - if (codePoint >= 0x300 && codePoint <= 0x36F) { + if (codePoint >= 0x3_00 && codePoint <= 0x3_6F) { continue; } const code = eastAsianWidth.eastAsianWidth(character); switch (code) { case 'F': - case 'W': + case 'W': { width += 2; break; - case 'A': + } + + case 'A': { width += ambiguousCharacterWidth; break; - default: + } + + default: { width += 1; + } } } diff --git a/package.json b/package.json index f46d677..3f697ed 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "type": "module", "exports": "./index.js", "engines": { - "node": ">=12" + "node": ">=16" }, "scripts": { "test": "xo && ava && tsd" @@ -48,12 +48,12 @@ ], "dependencies": { "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", + "emoji-regex": "^10.2.1", "strip-ansi": "^7.0.1" }, "devDependencies": { - "ava": "^3.15.0", - "tsd": "^0.14.0", - "xo": "^0.38.2" + "ava": "^5.2.0", + "tsd": "^0.28.1", + "xo": "^0.54.2" } } diff --git a/test.js b/test.js index df3f1c4..7e8e4b6 100644 --- a/test.js +++ b/test.js @@ -18,14 +18,17 @@ test('main', t => { t.is(stringWidth('\u{2194}\u{FE0F}'), 2, '↔️ default text presentation character rendered as emoji'); t.is(stringWidth('\u{1F469}'), 2, '👩 emoji modifier base (Emoji_Modifier_Base)'); t.is(stringWidth('\u{1F469}\u{1F3FF}'), 2, '👩🏿 emoji modifier base followed by a modifier'); + t.is(stringWidth('\u{845B}\u{E0100}'), 2, 'Variation Selectors'); + t.is(stringWidth('ปฏัก'), 3, 'Thai script'); + t.is(stringWidth('_\u0E34'), 1, 'Thai script'); }); test('ignores control characters', t => { - t.is(stringWidth(String.fromCharCode(0)), 0); - t.is(stringWidth(String.fromCharCode(31)), 0); - t.is(stringWidth(String.fromCharCode(127)), 0); - t.is(stringWidth(String.fromCharCode(134)), 0); - t.is(stringWidth(String.fromCharCode(159)), 0); + t.is(stringWidth(String.fromCodePoint(0)), 0); + t.is(stringWidth(String.fromCodePoint(31)), 0); + t.is(stringWidth(String.fromCodePoint(127)), 0); + t.is(stringWidth(String.fromCodePoint(134)), 0); + t.is(stringWidth(String.fromCodePoint(159)), 0); t.is(stringWidth('\u001B'), 0); });