Skip to content

Commit

Permalink
Added UiString.charStartPos
Browse files Browse the repository at this point in the history
  • Loading branch information
viktor-podzigun committed Mar 16, 2024
1 parent a4dc1e5 commit faf4630
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 5 deletions.
7 changes: 7 additions & 0 deletions src/UiString.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
export interface UiString {
strWidth(): number;
toString(): string;
charStartPos(from: number): UiCharStartPos;
slice(from: number, until: number): string;
ensureWidth(width: number, padCh: string): string;
}

export interface UiCharStartPos {
readonly lcw: number;
readonly pos: number;
readonly rcw: number;
}
27 changes: 23 additions & 4 deletions src/UiString.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @typedef {import('./UiString').UiCharStartPos} UiCharStartPos
*/
import Blessed from "@farjs/blessed";

const { unicode } = Blessed;
Expand Down Expand Up @@ -32,10 +35,10 @@ function UiString(str) {
cw = unicode.charWidth(str, i);

if (sw + cw <= width) {
if (
unicode.isSurrogate(str, i) ||
(i + 1 < str.length && unicode.isCombining(str, i + 1))
) {
if (unicode.isSurrogate(str, i)) {
i += 1;
}
while (i + 1 < str.length && unicode.charWidth(str, i + 1) === 0) {
i += 1;
}
i += 1;
Expand Down Expand Up @@ -86,6 +89,22 @@ function UiString(str) {

toString: () => str,

charStartPos: (from) => {
if (strWidth() === 0) {
return { lcw: 0, pos: 0, rcw: 0 };
}

const pos = Math.min(Math.max(from, 0), strWidth());
const { i, sw, cw } = skipWidth(0, pos === 0 ? 1 : pos);
if (pos === 0) {
return { lcw: 0, pos, rcw: cw };
}
const start = sw + cw;
const next = start > pos ? i + 1 : i;
const rcw = start === strWidth() ? 0 : skipWidth(next, 1).cw;
return { lcw: cw, pos: start, rcw };
},

slice: (from, until) => {
const start = from > 0 ? skipWidth(0, from).i : 0;
const { i: end } = skipWidth(start, until - from);
Expand Down
84 changes: 83 additions & 1 deletion test/UiString.test.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @typedef {import('../src/UiString').UiCharStartPos} UiCharStartPos
*/
import Blessed from "@farjs/blessed";
import assert from "node:assert/strict";
import UiString from "../src/UiString.mjs";
Expand Down Expand Up @@ -32,6 +35,60 @@ describe("UiString.test.mjs", () => {
assert.deepEqual(UiString(str).toString(), str);
});

it("should return left/right char widths and start pos when charStartPos", () => {
/**
* @param {string} str
* @param {number} pos
* @param {UiCharStartPos} expected
*/
function check(str, pos, expected) {
assert.deepEqual(UiString(str).charStartPos(pos), expected);
}

//when & then
check("", 0, { lcw: 0, pos: 0, rcw: 0 });
check("abc", -1, { lcw: 0, pos: 0, rcw: 1 });
check("abc", 0, { lcw: 0, pos: 0, rcw: 1 });
check("abc", 1, { lcw: 1, pos: 1, rcw: 1 });
check("abc", 2, { lcw: 1, pos: 2, rcw: 1 });
check("abc", 3, { lcw: 1, pos: 3, rcw: 0 });
check("abc", 4, { lcw: 1, pos: 3, rcw: 0 });
check("й", 0, { lcw: 0, pos: 0, rcw: 1 });
check("й", 1, { lcw: 1, pos: 1, rcw: 0 });
check("й", 2, { lcw: 1, pos: 1, rcw: 0 });
check("aй", 0, { lcw: 0, pos: 0, rcw: 1 });
check("aй", 1, { lcw: 1, pos: 1, rcw: 1 });
check("aй", 2, { lcw: 1, pos: 2, rcw: 0 });
check("йa", 0, { lcw: 0, pos: 0, rcw: 1 });
check("йa", 1, { lcw: 1, pos: 1, rcw: 1 });
check("йa", 2, { lcw: 1, pos: 2, rcw: 0 });
check("\uD83C\uDF31", 0, { lcw: 0, pos: 0, rcw: 2 });
check("\uD83C\uDF31", 1, { lcw: 2, pos: 2, rcw: 0 });
check("\uD83C\uDF31", 2, { lcw: 2, pos: 2, rcw: 0 });
check("a\uD83C\uDF31", 0, { lcw: 0, pos: 0, rcw: 1 });
check("a\uD83C\uDF31", 1, { lcw: 1, pos: 1, rcw: 2 });
check("a\uD83C\uDF31", 2, { lcw: 2, pos: 3, rcw: 0 });
check("a\uD83C\uDF31", 3, { lcw: 2, pos: 3, rcw: 0 });
check("\uff01", 0, { lcw: 0, pos: 0, rcw: 2 });
check("\uff01", 1, { lcw: 2, pos: 2, rcw: 0 });
check("\uff01", 2, { lcw: 2, pos: 2, rcw: 0 });
check("a\uff01b", 0, { lcw: 0, pos: 0, rcw: 1 });
check("a\uff01b", 1, { lcw: 1, pos: 1, rcw: 2 });
check("a\uff01b", 2, { lcw: 2, pos: 3, rcw: 1 });
check("a\uff01b", 3, { lcw: 2, pos: 3, rcw: 1 });
check("a\uff01b", 4, { lcw: 1, pos: 4, rcw: 0 });
check("\u200D", 0, { lcw: 0, pos: 0, rcw: 0 });
check("\u200Dй", 0, { lcw: 0, pos: 0, rcw: 1 });
check("\u200Dй", 1, { lcw: 1, pos: 1, rcw: 0 });
check("\u200Dй", 2, { lcw: 1, pos: 1, rcw: 0 });
check("\u200Dй\u200Dй", 0, { lcw: 0, pos: 0, rcw: 1 });
check("\u200Dй\u200Dй", 1, { lcw: 1, pos: 1, rcw: 1 });
check("\u200Dй\u200Dй", 2, { lcw: 1, pos: 2, rcw: 0 });
check("double 🉐", 4, { lcw: 1, pos: 4, rcw: 1 });
check("double 🉐", 7, { lcw: 1, pos: 7, rcw: 2 });
check("double 🉐", 8, { lcw: 2, pos: 9, rcw: 0 });
});

it("should return part of str when slice", () => {
//given
const str = "abcd";
Expand All @@ -54,6 +111,8 @@ describe("UiString.test.mjs", () => {

it("should handle combining chars when slice", () => {
//given
assert.deepEqual(unicode.isCombining("\u200D", 0), true);
assert.deepEqual(unicode.charWidth("\u200D", 0), 0);
assert.deepEqual(unicode.isCombining("й", 0), false);
assert.deepEqual(unicode.isCombining("й", 1), true);
assert.deepEqual(unicode.strWidth("й"), 1);
Expand All @@ -63,14 +122,37 @@ describe("UiString.test.mjs", () => {
assert.deepEqual(UiString("Валютный").slice(6, 7), "ы");
assert.deepEqual(UiString("Валютный").slice(7, 8), "й");
assert.deepEqual(UiString("й").slice(0, 1), "й");
assert.deepEqual(UiString("й\u200D").slice(0, 1), "й\u200D");
assert.deepEqual(UiString("й\u200D\u200Db").slice(0, 1), "й\u200D\u200D");
assert.deepEqual(UiString("й\u200D\u200Db").slice(1, 2), "b");
assert.deepEqual(UiString("1й").slice(0, 1), "1");
assert.deepEqual(UiString("1й").slice(0, 2), "1й");
assert.deepEqual(UiString("й2").slice(0, 2), "й2");
assert.deepEqual(UiString("й2").slice(0, 1), "й");
assert.deepEqual(UiString("й2").slice(1, 2), "2");
});

it("should handle surrogate chars when slice", () => {
it("should handle high/low surrogate chars when slice", () => {
//given
assert.deepEqual(unicode.isSurrogate("\uD83C", 0), false);
assert.deepEqual(unicode.isSurrogate("\uD83Ca", 0), false);
assert.deepEqual(unicode.isSurrogate("\uD83C\uDF31", 0), true);
assert.deepEqual(unicode.isSurrogate("\uD83C\uDF31", 1), false);
assert.deepEqual(unicode.charWidth("\uD800", 0), 0);
assert.deepEqual(unicode.charWidth("\uD83C", 0), 0);
assert.deepEqual(unicode.charWidth("\uDBFF", 0), 0);
assert.deepEqual(unicode.charWidth("\uDC00", 0), 0);
assert.deepEqual(unicode.charWidth("\uDFFF", 0), 0);
assert.deepEqual(unicode.isCombining("\uD83C", 0), false);

//when & then
assert.deepEqual(UiString("\uD83C").slice(0, 1), "\uD83C");
assert.deepEqual(UiString("a\uD83C").slice(0, 1), "a\uD83C");
assert.deepEqual(UiString("a\uD83C\uD800b").slice(0, 1), "a\uD83C\uD800");
assert.deepEqual(UiString("a\uD83C\uD800b").slice(1, 2), "b");
});

it("should handle surrogate pairs when slice", () => {
//given
assert.deepEqual(unicode.isSurrogate("\uD83C\uDF31", 0), true);
assert.deepEqual(unicode.isSurrogate("\uD83C\uDF31", 1), false);
Expand Down

0 comments on commit faf4630

Please sign in to comment.