Skip to content

Commit faf4630

Browse files
Added UiString.charStartPos
1 parent a4dc1e5 commit faf4630

File tree

3 files changed

+113
-5
lines changed

3 files changed

+113
-5
lines changed

src/UiString.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
export interface UiString {
22
strWidth(): number;
33
toString(): string;
4+
charStartPos(from: number): UiCharStartPos;
45
slice(from: number, until: number): string;
56
ensureWidth(width: number, padCh: string): string;
67
}
8+
9+
export interface UiCharStartPos {
10+
readonly lcw: number;
11+
readonly pos: number;
12+
readonly rcw: number;
13+
}

src/UiString.mjs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/**
2+
* @typedef {import('./UiString').UiCharStartPos} UiCharStartPos
3+
*/
14
import Blessed from "@farjs/blessed";
25

36
const { unicode } = Blessed;
@@ -32,10 +35,10 @@ function UiString(str) {
3235
cw = unicode.charWidth(str, i);
3336

3437
if (sw + cw <= width) {
35-
if (
36-
unicode.isSurrogate(str, i) ||
37-
(i + 1 < str.length && unicode.isCombining(str, i + 1))
38-
) {
38+
if (unicode.isSurrogate(str, i)) {
39+
i += 1;
40+
}
41+
while (i + 1 < str.length && unicode.charWidth(str, i + 1) === 0) {
3942
i += 1;
4043
}
4144
i += 1;
@@ -86,6 +89,22 @@ function UiString(str) {
8689

8790
toString: () => str,
8891

92+
charStartPos: (from) => {
93+
if (strWidth() === 0) {
94+
return { lcw: 0, pos: 0, rcw: 0 };
95+
}
96+
97+
const pos = Math.min(Math.max(from, 0), strWidth());
98+
const { i, sw, cw } = skipWidth(0, pos === 0 ? 1 : pos);
99+
if (pos === 0) {
100+
return { lcw: 0, pos, rcw: cw };
101+
}
102+
const start = sw + cw;
103+
const next = start > pos ? i + 1 : i;
104+
const rcw = start === strWidth() ? 0 : skipWidth(next, 1).cw;
105+
return { lcw: cw, pos: start, rcw };
106+
},
107+
89108
slice: (from, until) => {
90109
const start = from > 0 ? skipWidth(0, from).i : 0;
91110
const { i: end } = skipWidth(start, until - from);

test/UiString.test.mjs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/**
2+
* @typedef {import('../src/UiString').UiCharStartPos} UiCharStartPos
3+
*/
14
import Blessed from "@farjs/blessed";
25
import assert from "node:assert/strict";
36
import UiString from "../src/UiString.mjs";
@@ -32,6 +35,60 @@ describe("UiString.test.mjs", () => {
3235
assert.deepEqual(UiString(str).toString(), str);
3336
});
3437

38+
it("should return left/right char widths and start pos when charStartPos", () => {
39+
/**
40+
* @param {string} str
41+
* @param {number} pos
42+
* @param {UiCharStartPos} expected
43+
*/
44+
function check(str, pos, expected) {
45+
assert.deepEqual(UiString(str).charStartPos(pos), expected);
46+
}
47+
48+
//when & then
49+
check("", 0, { lcw: 0, pos: 0, rcw: 0 });
50+
check("abc", -1, { lcw: 0, pos: 0, rcw: 1 });
51+
check("abc", 0, { lcw: 0, pos: 0, rcw: 1 });
52+
check("abc", 1, { lcw: 1, pos: 1, rcw: 1 });
53+
check("abc", 2, { lcw: 1, pos: 2, rcw: 1 });
54+
check("abc", 3, { lcw: 1, pos: 3, rcw: 0 });
55+
check("abc", 4, { lcw: 1, pos: 3, rcw: 0 });
56+
check("й", 0, { lcw: 0, pos: 0, rcw: 1 });
57+
check("й", 1, { lcw: 1, pos: 1, rcw: 0 });
58+
check("й", 2, { lcw: 1, pos: 1, rcw: 0 });
59+
check("aй", 0, { lcw: 0, pos: 0, rcw: 1 });
60+
check("aй", 1, { lcw: 1, pos: 1, rcw: 1 });
61+
check("aй", 2, { lcw: 1, pos: 2, rcw: 0 });
62+
check("йa", 0, { lcw: 0, pos: 0, rcw: 1 });
63+
check("йa", 1, { lcw: 1, pos: 1, rcw: 1 });
64+
check("йa", 2, { lcw: 1, pos: 2, rcw: 0 });
65+
check("\uD83C\uDF31", 0, { lcw: 0, pos: 0, rcw: 2 });
66+
check("\uD83C\uDF31", 1, { lcw: 2, pos: 2, rcw: 0 });
67+
check("\uD83C\uDF31", 2, { lcw: 2, pos: 2, rcw: 0 });
68+
check("a\uD83C\uDF31", 0, { lcw: 0, pos: 0, rcw: 1 });
69+
check("a\uD83C\uDF31", 1, { lcw: 1, pos: 1, rcw: 2 });
70+
check("a\uD83C\uDF31", 2, { lcw: 2, pos: 3, rcw: 0 });
71+
check("a\uD83C\uDF31", 3, { lcw: 2, pos: 3, rcw: 0 });
72+
check("\uff01", 0, { lcw: 0, pos: 0, rcw: 2 });
73+
check("\uff01", 1, { lcw: 2, pos: 2, rcw: 0 });
74+
check("\uff01", 2, { lcw: 2, pos: 2, rcw: 0 });
75+
check("a\uff01b", 0, { lcw: 0, pos: 0, rcw: 1 });
76+
check("a\uff01b", 1, { lcw: 1, pos: 1, rcw: 2 });
77+
check("a\uff01b", 2, { lcw: 2, pos: 3, rcw: 1 });
78+
check("a\uff01b", 3, { lcw: 2, pos: 3, rcw: 1 });
79+
check("a\uff01b", 4, { lcw: 1, pos: 4, rcw: 0 });
80+
check("\u200D", 0, { lcw: 0, pos: 0, rcw: 0 });
81+
check("\u200Dй", 0, { lcw: 0, pos: 0, rcw: 1 });
82+
check("\u200Dй", 1, { lcw: 1, pos: 1, rcw: 0 });
83+
check("\u200Dй", 2, { lcw: 1, pos: 1, rcw: 0 });
84+
check("\u200Dй\u200Dй", 0, { lcw: 0, pos: 0, rcw: 1 });
85+
check("\u200Dй\u200Dй", 1, { lcw: 1, pos: 1, rcw: 1 });
86+
check("\u200Dй\u200Dй", 2, { lcw: 1, pos: 2, rcw: 0 });
87+
check("double 🉐", 4, { lcw: 1, pos: 4, rcw: 1 });
88+
check("double 🉐", 7, { lcw: 1, pos: 7, rcw: 2 });
89+
check("double 🉐", 8, { lcw: 2, pos: 9, rcw: 0 });
90+
});
91+
3592
it("should return part of str when slice", () => {
3693
//given
3794
const str = "abcd";
@@ -54,6 +111,8 @@ describe("UiString.test.mjs", () => {
54111

55112
it("should handle combining chars when slice", () => {
56113
//given
114+
assert.deepEqual(unicode.isCombining("\u200D", 0), true);
115+
assert.deepEqual(unicode.charWidth("\u200D", 0), 0);
57116
assert.deepEqual(unicode.isCombining("й", 0), false);
58117
assert.deepEqual(unicode.isCombining("й", 1), true);
59118
assert.deepEqual(unicode.strWidth("й"), 1);
@@ -63,14 +122,37 @@ describe("UiString.test.mjs", () => {
63122
assert.deepEqual(UiString("Валютный").slice(6, 7), "ы");
64123
assert.deepEqual(UiString("Валютный").slice(7, 8), "й");
65124
assert.deepEqual(UiString("й").slice(0, 1), "й");
125+
assert.deepEqual(UiString("й\u200D").slice(0, 1), "й\u200D");
126+
assert.deepEqual(UiString("й\u200D\u200Db").slice(0, 1), "й\u200D\u200D");
127+
assert.deepEqual(UiString("й\u200D\u200Db").slice(1, 2), "b");
66128
assert.deepEqual(UiString("1й").slice(0, 1), "1");
67129
assert.deepEqual(UiString("1й").slice(0, 2), "1й");
68130
assert.deepEqual(UiString("й2").slice(0, 2), "й2");
69131
assert.deepEqual(UiString("й2").slice(0, 1), "й");
70132
assert.deepEqual(UiString("й2").slice(1, 2), "2");
71133
});
72134

73-
it("should handle surrogate chars when slice", () => {
135+
it("should handle high/low surrogate chars when slice", () => {
136+
//given
137+
assert.deepEqual(unicode.isSurrogate("\uD83C", 0), false);
138+
assert.deepEqual(unicode.isSurrogate("\uD83Ca", 0), false);
139+
assert.deepEqual(unicode.isSurrogate("\uD83C\uDF31", 0), true);
140+
assert.deepEqual(unicode.isSurrogate("\uD83C\uDF31", 1), false);
141+
assert.deepEqual(unicode.charWidth("\uD800", 0), 0);
142+
assert.deepEqual(unicode.charWidth("\uD83C", 0), 0);
143+
assert.deepEqual(unicode.charWidth("\uDBFF", 0), 0);
144+
assert.deepEqual(unicode.charWidth("\uDC00", 0), 0);
145+
assert.deepEqual(unicode.charWidth("\uDFFF", 0), 0);
146+
assert.deepEqual(unicode.isCombining("\uD83C", 0), false);
147+
148+
//when & then
149+
assert.deepEqual(UiString("\uD83C").slice(0, 1), "\uD83C");
150+
assert.deepEqual(UiString("a\uD83C").slice(0, 1), "a\uD83C");
151+
assert.deepEqual(UiString("a\uD83C\uD800b").slice(0, 1), "a\uD83C\uD800");
152+
assert.deepEqual(UiString("a\uD83C\uD800b").slice(1, 2), "b");
153+
});
154+
155+
it("should handle surrogate pairs when slice", () => {
74156
//given
75157
assert.deepEqual(unicode.isSurrogate("\uD83C\uDF31", 0), true);
76158
assert.deepEqual(unicode.isSurrogate("\uD83C\uDF31", 1), false);

0 commit comments

Comments
 (0)