Skip to content

Commit f85812f

Browse files
authored
Use Intl.Segmenter, require Node.js v16 (#47)
1 parent 9f90691 commit f85812f

File tree

5 files changed

+41
-23
lines changed

5 files changed

+41
-23
lines changed

.github/workflows/main.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ jobs:
1010
fail-fast: false
1111
matrix:
1212
node-version:
13-
- 14
14-
- 12
13+
- 20
14+
- 18
15+
- 16
1516
steps:
16-
- uses: actions/checkout@v2
17-
- uses: actions/setup-node@v2
17+
- uses: actions/checkout@v3
18+
- uses: actions/setup-node@v3
1819
with:
1920
node-version: ${{ matrix.node-version }}
2021
- run: npm install

index.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
export interface Options {
1+
export type Options = {
22
/**
33
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).
44
55
@default true
66
*/
77
readonly ambiguousIsNarrow: boolean;
8-
}
8+
};
99

1010
/**
1111
Get the visual width of a string - the number of columns required to display it.

index.js

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@ import stripAnsi from 'strip-ansi';
22
import eastAsianWidth from 'eastasianwidth';
33
import emojiRegex from 'emoji-regex';
44

5-
export default function stringWidth(string, options = {}) {
5+
let segmenter;
6+
function * splitString(string) {
7+
segmenter ??= new Intl.Segmenter();
8+
9+
for (const {segment: character} of segmenter.segment(string)) {
10+
yield character;
11+
}
12+
}
13+
14+
export default function stringWidth(string, options) {
615
if (typeof string !== 'string' || string.length === 0) {
716
return 0;
817
}
918

1019
options = {
1120
ambiguousIsNarrow: true,
12-
...options
21+
...options,
1322
};
1423

1524
string = stripAnsi(string);
@@ -23,7 +32,7 @@ export default function stringWidth(string, options = {}) {
2332
const ambiguousCharacterWidth = options.ambiguousIsNarrow ? 1 : 2;
2433
let width = 0;
2534

26-
for (const character of string) {
35+
for (const character of splitString(string)) {
2736
const codePoint = character.codePointAt(0);
2837

2938
// Ignore control characters
@@ -32,21 +41,26 @@ export default function stringWidth(string, options = {}) {
3241
}
3342

3443
// Ignore combining characters
35-
if (codePoint >= 0x300 && codePoint <= 0x36F) {
44+
if (codePoint >= 0x3_00 && codePoint <= 0x3_6F) {
3645
continue;
3746
}
3847

3948
const code = eastAsianWidth.eastAsianWidth(character);
4049
switch (code) {
4150
case 'F':
42-
case 'W':
51+
case 'W': {
4352
width += 2;
4453
break;
45-
case 'A':
54+
}
55+
56+
case 'A': {
4657
width += ambiguousCharacterWidth;
4758
break;
48-
default:
59+
}
60+
61+
default: {
4962
width += 1;
63+
}
5064
}
5165
}
5266

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"type": "module",
1414
"exports": "./index.js",
1515
"engines": {
16-
"node": ">=12"
16+
"node": ">=16"
1717
},
1818
"scripts": {
1919
"test": "xo && ava && tsd"
@@ -48,12 +48,12 @@
4848
],
4949
"dependencies": {
5050
"eastasianwidth": "^0.2.0",
51-
"emoji-regex": "^9.2.2",
51+
"emoji-regex": "^10.2.1",
5252
"strip-ansi": "^7.0.1"
5353
},
5454
"devDependencies": {
55-
"ava": "^3.15.0",
56-
"tsd": "^0.14.0",
57-
"xo": "^0.38.2"
55+
"ava": "^5.2.0",
56+
"tsd": "^0.28.1",
57+
"xo": "^0.54.2"
5858
}
5959
}

test.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@ test('main', t => {
1818
t.is(stringWidth('\u{2194}\u{FE0F}'), 2, '↔️ default text presentation character rendered as emoji');
1919
t.is(stringWidth('\u{1F469}'), 2, '👩 emoji modifier base (Emoji_Modifier_Base)');
2020
t.is(stringWidth('\u{1F469}\u{1F3FF}'), 2, '👩🏿 emoji modifier base followed by a modifier');
21+
t.is(stringWidth('\u{845B}\u{E0100}'), 2, 'Variation Selectors');
22+
t.is(stringWidth('ปฏัก'), 3, 'Thai script');
23+
t.is(stringWidth('_\u0E34'), 1, 'Thai script');
2124
});
2225

2326
test('ignores control characters', t => {
24-
t.is(stringWidth(String.fromCharCode(0)), 0);
25-
t.is(stringWidth(String.fromCharCode(31)), 0);
26-
t.is(stringWidth(String.fromCharCode(127)), 0);
27-
t.is(stringWidth(String.fromCharCode(134)), 0);
28-
t.is(stringWidth(String.fromCharCode(159)), 0);
27+
t.is(stringWidth(String.fromCodePoint(0)), 0);
28+
t.is(stringWidth(String.fromCodePoint(31)), 0);
29+
t.is(stringWidth(String.fromCodePoint(127)), 0);
30+
t.is(stringWidth(String.fromCodePoint(134)), 0);
31+
t.is(stringWidth(String.fromCodePoint(159)), 0);
2932
t.is(stringWidth('\u001B'), 0);
3033
});
3134

0 commit comments

Comments
 (0)