Skip to content

Commit f16f943

Browse files
kevvasindresorhus
authored andcommittedJul 22, 2017
Add support for surrogate pairs and full width characters (#20)
1 parent 3483a30 commit f16f943

File tree

2 files changed

+42
-29
lines changed

2 files changed

+42
-29
lines changed
 

‎index.js

+38-24
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
const stringWidth = require('string-width');
33
const stripAnsi = require('strip-ansi');
44

5-
const ESCAPES = [
5+
const ESCAPES = new Set([
66
'\u001B',
77
'\u009B'
8-
];
8+
]);
99

1010
const END_CODE = 39;
1111

@@ -37,26 +37,35 @@ const ESCAPE_CODES = new Map([
3737
[47, 49]
3838
]);
3939

40-
const wrapAnsi = code => `${ESCAPES[0]}[${code}m`;
40+
const wrapAnsi = code => `${ESCAPES.values().next().value}[${code}m`;
4141

4242
// Calculate the length of words split on ' ', ignoring
4343
// the extra characters added by ansi escape codes
4444
const wordLengths = str => str.split(' ').map(s => stringWidth(s));
4545

4646
// Wrap a long word across multiple rows
4747
// Ansi escape codes do not count towards length
48-
function wrapWord(rows, word, cols) {
48+
const wrapWord = (rows, word, cols) => {
49+
const arr = Array.from(word);
50+
4951
let insideEscape = false;
50-
let visible = stripAnsi(rows[rows.length - 1]).length;
52+
let visible = stringWidth(stripAnsi(rows[rows.length - 1]));
5153

52-
for (let i = 0; i < word.length; i++) {
53-
const x = word[i];
54+
for (const item of arr.entries()) {
55+
const i = item[0];
56+
const char = item[1];
57+
const charLength = stringWidth(char);
5458

55-
rows[rows.length - 1] += x;
59+
if (visible + charLength <= cols) {
60+
rows[rows.length - 1] += char;
61+
} else {
62+
rows.push(char);
63+
visible = 0;
64+
}
5665

57-
if (ESCAPES.indexOf(x) !== -1) {
66+
if (ESCAPES.has(char)) {
5867
insideEscape = true;
59-
} else if (insideEscape && x === 'm') {
68+
} else if (insideEscape && char === 'm') {
6069
insideEscape = false;
6170
continue;
6271
}
@@ -65,9 +74,9 @@ function wrapWord(rows, word, cols) {
6574
continue;
6675
}
6776

68-
visible++;
77+
visible += charLength;
6978

70-
if (visible >= cols && i < word.length - 1) {
79+
if (visible === cols && i < arr.length - 1) {
7180
rows.push('');
7281
visible = 0;
7382
}
@@ -78,7 +87,7 @@ function wrapWord(rows, word, cols) {
7887
if (!visible && rows[rows.length - 1].length > 0 && rows.length > 1) {
7988
rows[rows.length - 2] += rows.pop();
8089
}
81-
}
90+
};
8291

8392
// The wrap-ansi module can be invoked
8493
// in either 'hard' or 'soft' wrap mode
@@ -87,7 +96,7 @@ function wrapWord(rows, word, cols) {
8796
// than cols characters
8897
//
8998
// 'soft' allows long words to expand past the column length
90-
function exec(str, cols, opts) {
99+
const exec = (str, cols, opts) => {
91100
const options = opts || {};
92101

93102
let pre = '';
@@ -98,7 +107,10 @@ function exec(str, cols, opts) {
98107
const words = str.split(' ');
99108
const rows = [''];
100109

101-
for (let i = 0, word; (word = words[i]) !== undefined; i++) {
110+
for (const item of Array.from(words).entries()) {
111+
const i = item[0];
112+
const word = item[1];
113+
102114
let rowLength = stringWidth(rows[rows.length - 1]);
103115

104116
if (rowLength) {
@@ -135,33 +147,35 @@ function exec(str, cols, opts) {
135147

136148
pre = rows.map(x => x.trim()).join('\n');
137149

138-
for (let j = 0; j < pre.length; j++) {
139-
const y = pre[j];
150+
for (const item of Array.from(pre).entries()) {
151+
const i = item[0];
152+
const char = item[1];
140153

141-
ret += y;
154+
ret += char;
142155

143-
if (ESCAPES.indexOf(y) !== -1) {
144-
const code = parseFloat(/\d[^m]*/.exec(pre.slice(j, j + 4)));
156+
if (ESCAPES.has(char)) {
157+
const code = parseFloat(/\d[^m]*/.exec(pre.slice(i, i + 4)));
145158
escapeCode = code === END_CODE ? null : code;
146159
}
147160

148-
const code = ESCAPE_CODES.get(parseInt(escapeCode, 10));
161+
const code = ESCAPE_CODES.get(Number(escapeCode));
149162

150163
if (escapeCode && code) {
151-
if (pre[j + 1] === '\n') {
164+
if (pre[i + 1] === '\n') {
152165
ret += wrapAnsi(code);
153-
} else if (y === '\n') {
166+
} else if (char === '\n') {
154167
ret += wrapAnsi(escapeCode);
155168
}
156169
}
157170
}
158171

159172
return ret;
160-
}
173+
};
161174

162175
// For each newline, invoke the method separately
163176
module.exports = (str, cols, opts) => {
164177
return String(str)
178+
.normalize()
165179
.split('\n')
166180
.map(line => exec(line, cols, opts))
167181
.join('\n');

‎test.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,11 @@ test('no word-wrapping', t => {
8686
t.is(res3, 'The q\nuick\nbrown\n\u001B[31mfox j\u001B[39m\n\u001B[31mumped\u001B[39m\n\u001B[31mover\u001B[39m\n\u001B[31m\u001B[39mthe l\nazy \u001B[32md\u001B[39m\n\u001B[32mog an\u001B[39m\n\u001B[32md the\u001B[39m\n\u001B[32mn ran\u001B[39m\n\u001B[32maway\u001B[39m\n\u001B[32mwith\u001B[39m\n\u001B[32mthe u\u001B[39m\n\u001B[32mnicor\u001B[39m\n\u001B[32mn.\u001B[39m');
8787
});
8888

89-
// https://github.com/chalk/wrap-ansi/issues/10
90-
test.failing('supports fullwidth characters', t => {
89+
test('supports fullwidth characters', t => {
9190
t.is(m('안녕하세', 4, {hard: true}), '안녕\n하세');
9291
});
9392

94-
// https://github.com/chalk/wrap-ansi/issues/11
95-
test.failing('supports unicode surrogate pairs', t => {
96-
t.is(m('a\ud83c\ude00bc', 2, {hard: true}), 'a\n\ud83c\ude00\nbc');
93+
test('supports unicode surrogate pairs', t => {
94+
t.is(m('a\uD83C\uDE00bc', 2, {hard: true}), 'a\n\uD83C\uDE00\nbc');
95+
t.is(m('a\uD83C\uDE00bc\uD83C\uDE00d\uD83C\uDE00', 2, {hard: true}), 'a\n\uD83C\uDE00\nbc\n\uD83C\uDE00\nd\n\uD83C\uDE00');
9796
});

0 commit comments

Comments
 (0)
Please sign in to comment.