Skip to content

Commit c7b9506

Browse files
authored
Merge pull request #2 from cto-af/break
Fix break logic to be more useful
2 parents a8aa69c + 7feba06 commit c7b9506

File tree

3 files changed

+67
-22
lines changed

3 files changed

+67
-22
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const sw = new StringWidth()
1919
sw.width('foo') // 3
2020
sw.width('\u{1F4A9}') // 2: Emoji take two cells
2121
sw.width('#\ufe0f\u20e3') // 2: More complicated emoji
22-
sw.break('foobar', 3) // ['foo', 'bar']
22+
sw.break('foobar', 3) // [{string: 'foo', cells: 3}, {string: 'bar', cells: 3}]
2323

2424
const custom = new StringWidth({
2525
locale: 'ko-KR',

lib/index.js

+44-15
Original file line numberDiff line numberDiff line change
@@ -142,30 +142,59 @@ export class StringWidth {
142142
}
143143

144144
/**
145-
* Break a string into two parts, such that the first part is always
146-
* shorter or equal to width in display cells.
145+
* Break a string into multiple parts, such that each part has maximum
146+
* cell length of width, unless a particular grapheme cluster is longer
147+
* than width. Therefore, width should probably be 4 or higher to get
148+
* the expected results.
147149
*
148150
* @param {string} str String to segment
149-
* @param {number} width Maximum number of display cells for first chunk
150-
* @returns {[string, string]}
151+
* @param {number} width Maximum number of display cells for chunks
152+
* @returns {{string: string, cells: number}[]}
151153
*/
152154
break(str, width) {
155+
if (width < 1) {
156+
throw new RangeError(`Width must be >= 1. Got ${width}.`)
157+
}
158+
159+
/** @type {{string: string, cells: number}[]} */
160+
const ret = []
161+
let string = ''
162+
let cells = 0
163+
153164
// Fast-path shortcut for all-ASCII
154165
if (ASCII_REGEX.test(str)) {
155-
return [str.slice(0, width), str.slice(width)]
166+
for (cells = 0; cells < str.length; cells += width) {
167+
string = str.slice(cells, cells + width)
168+
ret.push({
169+
string,
170+
cells: string.length, // Might be less than width for last chunk
171+
})
172+
}
173+
return ret
156174
}
157175

158-
let ret = ''
159-
let rest = ''
160-
let cur = 0 // Cells
161-
for (const {segment, index} of this.#graphemes.segment(str)) {
162-
cur += this.#segmentWidth(segment)
163-
if (cur > width) { // Cells
164-
rest = str.slice(index + segment.length - 1) // Chars
165-
break
176+
for (const segment of this.graphemes(str)) {
177+
const w = this.#segmentWidth(segment)
178+
if (w > width) {
179+
// Skinny width, fat grapheme cluster
180+
if (cells > 0) {
181+
ret.push({string, cells})
182+
string = ''
183+
cells = 0
184+
}
185+
ret.push({string: segment, cells: w})
186+
} else if (cells + w > width) {
187+
ret.push({string, cells})
188+
string = segment
189+
cells = w
190+
} else {
191+
string += segment
192+
cells += w
166193
}
167-
ret += segment
168194
}
169-
return [ret, rest]
195+
if (cells > 0) {
196+
ret.push({string, cells})
197+
}
198+
return ret
170199
}
171200
}

test/index.test.js

+22-6
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,29 @@ describe('info', () => {
5353

5454
describe('string breaks', () => {
5555
it('handles ascii only', () => {
56-
assert.deepEqual(sw.break('foo', 10), ['foo', ''])
57-
assert.deepEqual(sw.break('foobar', 3), ['foo', 'bar'])
58-
assert.deepEqual(sw.break('foo', 0), ['', 'foo'])
56+
assert.deepEqual(sw.break('foo', 10), [{string: 'foo', cells: 3}])
57+
assert.deepEqual(sw.break('foobar', 3), [
58+
{string: 'foo', cells: 3},
59+
{string: 'bar', cells: 3},
60+
])
61+
assert.throws(() => sw.break('foo', 0))
5962
})
6063
it('handles non-ascii', () => {
61-
assert.deepEqual(sw.break('foo\u0308', 10), ['foo\u0308', ''])
62-
assert.deepEqual(sw.break('foo\u0308ba\u0308r', 3), ['foo\u0308', 'ba\u0308r'])
63-
assert.deepEqual(sw.break('foo\u0308', 0), ['', 'foo\u0308'])
64+
assert.deepEqual(sw.break('foo\u0308', 10), [
65+
{string: 'foo\u0308', cells: 3},
66+
])
67+
assert.deepEqual(sw.break('foo\u0308ba\u0308r', 3), [
68+
{string: 'foo\u0308', cells: 3},
69+
{string: 'ba\u0308r', cells: 3},
70+
])
71+
assert.deepEqual(sw.break('\u{1F1F9}\u{1F1FC}b', 1), [
72+
{string: '\u{1F1F9}\u{1F1FC}', cells: 2},
73+
{string: 'b', cells: 1},
74+
])
75+
assert.deepEqual(sw.break('a\u{1F1F9}\u{1F1FC}b', 1), [
76+
{string: 'a', cells: 1},
77+
{string: '\u{1F1F9}\u{1F1FC}', cells: 2},
78+
{string: 'b', cells: 1},
79+
])
6480
})
6581
})

0 commit comments

Comments
 (0)