Skip to content

Commit

Permalink
Added UiString
Browse files Browse the repository at this point in the history
  • Loading branch information
viktor-podzigun committed Jan 4, 2024
1 parent a0a8c43 commit f07c939
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 1 deletion.
2 changes: 1 addition & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"bun": ">=0.8"
},
"dependencies": {
"@farjs/blessed": "0.2.7",
"@farjs/blessed": "0.2.8",
"react": "^17.0.1",
"react-blessed": "0.7.2"
},
Expand Down
6 changes: 6 additions & 0 deletions ui/src/UiString.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface UiString {
readonly strWidth(): number;
readonly toString(): string;
readonly slice(from: number, until: number): string;
readonly ensureWidth(width: number, padCh: string): string;
}
100 changes: 100 additions & 0 deletions ui/src/UiString.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import Blessed from "@farjs/blessed";

const { unicode } = Blessed;

/**
* @param {string} str
* @returns {import('./UiString').UiString}
*/
function UiString(str) {
/** @type {number | undefined} */
let _strWidth = undefined;

function strWidth() {
if (_strWidth === undefined) {
_strWidth = unicode.strWidth(str);
}

return _strWidth;
}

/**
* @param {number} index
* @param {number} width
* @returns {{i: number, sw: number, cw: number}}
*/
function skipWidth(index, width) {
let sw = 0;
let cw = 0;
let i = index;
while (sw + cw < width && i < str.length) {
sw += cw;
cw = unicode.charWidth(str, i);

if (sw + cw <= width) {
if (
unicode.isSurrogate(str, i) ||
(i + 1 < str.length && unicode.isCombining(str, i + 1))
) {
i += 1;
}
i += 1;
}
}

return { i, sw, cw };
}

/**
* @param {number} width
* @param {string} padCh
* @returns {string}
*/
function ensureWidth(width, padCh) {
/**
* @param {string} s
* @param {number} padLen
* @returns {string}
*/
function pad(s, padLen) {
const buff = [s];
let count = padLen;
while (count > 0) {
buff.push(padCh);
count -= 1;
}
return buff.join("");
}

if (width === strWidth()) {
return str;
}
if (width > strWidth()) {
return pad(str, width - strWidth());
}

const { i, sw, cw } = skipWidth(0, width);
const s = str.slice(0, i);
if (sw + cw > width) {
return pad(s, width - sw);
}
return s;
}

return {
strWidth,

toString: () => str,

slice: (from, until) => {
const start = from > 0 ? skipWidth(0, from).i : 0;
const { i: end } = skipWidth(start, until - from);

return start >= end ? "" : str.substring(start, end);
},

ensureWidth,
};
}

export default UiString;
158 changes: 158 additions & 0 deletions ui/test/UiString.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import Blessed from "@farjs/blessed";
import assert from "node:assert/strict";
import UiString from "../src/UiString.mjs";

const { unicode } = Blessed;

const { describe, it } = await (async () => {
// @ts-ignore
const module = process.isBun ? "bun:test" : "node:test";
// @ts-ignore
return process.isBun // @ts-ignore
? Promise.resolve({ describe: (_, fn) => fn(), it: test })
: import(module);
})();

describe("UiString.test.mjs", () => {
it("should return str width when strWidth", () => {
//when & then
assert.deepEqual("Валютный".length, 9);
assert.deepEqual(UiString("Валютный").strWidth(), 8);
assert.deepEqual(UiString("\t").strWidth(), 8);
assert.deepEqual(UiString("\u0000\u001b\r\n").strWidth(), 0);
assert.deepEqual("\uD83C\uDF31-".length, 3);
assert.deepEqual(UiString("\uD83C\uDF31-").strWidth(), 3);
});

it("should return current str when toString", () => {
//given
const str = "test";

//when & then
assert.deepEqual(UiString(str).toString(), str);
});

it("should return part of str when slice", () => {
//given
const str = "abcd";

//when & then
assert.deepEqual(UiString(str).slice(0, 4), str);
assert.deepEqual(UiString(str).slice(-1, 5), str);
assert.deepEqual(UiString(str).slice(0, -1), "");
assert.deepEqual(UiString(str).slice(0, 0), "");
assert.deepEqual(UiString(str).slice(0, 1), "a");
assert.deepEqual(UiString(str).slice(0, 2), "ab");
assert.deepEqual(UiString(str).slice(1, 1), "");
assert.deepEqual(UiString(str).slice(1, 2), "b");
assert.deepEqual(UiString(str).slice(1, 3), "bc");
assert.deepEqual(UiString(str).slice(3, 2), "");
assert.deepEqual(UiString(str).slice(3, 3), "");
assert.deepEqual(UiString(str).slice(3, 4), "d");
assert.deepEqual(UiString("").slice(0, 1), "");
});

it("should handle combining chars when slice", () => {
//given
assert.deepEqual(unicode.isCombining("й", 0), false);
assert.deepEqual(unicode.isCombining("й", 1), true);
assert.deepEqual(unicode.strWidth("й"), 1);

//when & then
assert.deepEqual(UiString("Валютный").slice(0, 8), "Валютный");
assert.deepEqual(UiString("Валютный").slice(6, 7), "ы");
assert.deepEqual(UiString("Валютный").slice(7, 8), "й");
assert.deepEqual(UiString("й").slice(0, 1), "й");
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", () => {
//given
assert.deepEqual(unicode.isSurrogate("\uD83C\uDF31", 0), true);
assert.deepEqual(unicode.isSurrogate("\uD83C\uDF31", 1), false);
assert.deepEqual(unicode.charWidth("\uD83C\uDF31", 0), 2);
assert.deepEqual(unicode.charWidth("\uD83C\uDF31", 1), 0);
assert.deepEqual(unicode.strWidth("\uD83C\uDF31"), 2);
assert.deepEqual(unicode.strWidth("\u200D"), 0);
assert.deepEqual(unicode.strWidth("♂️"), 1);
assert.deepEqual(unicode.strWidth("\uD83E\uDD26\uD83C\uDFFC\u200D♂️"), 5);

//when & then
assert.deepEqual(UiString("\uD800\uDC002").slice(0, 2), "\uD800\uDC00");
assert.deepEqual(UiString("\uD800\uDC002").slice(0, 1), "");
assert.deepEqual(UiString("\uD800\uDC002").slice(1, 2), "");
assert.deepEqual(UiString("\uD83C\uDF31-").slice(0, 1), "");
assert.deepEqual(UiString("\uD83C\uDF31-").slice(0, 2), "\uD83C\uDF31");
});

it("should handle double-wide chars when slice", () => {
//given
assert.deepEqual(unicode.charWidth("\uff01", 0), 2);

//when & then
assert.deepEqual(UiString("te\uff012").slice(0, 5), "te\uff012");
assert.deepEqual(UiString("te\uff012").slice(0, 4), "te\uff01");
assert.deepEqual(UiString("te\uff012").slice(0, 3), "te");
assert.deepEqual(UiString("te\uff012").slice(0, 2), "te");
assert.deepEqual(UiString("te\uff012").slice(0, 1), "t");
assert.deepEqual(UiString("te\uff012").slice(1, 2), "e");
assert.deepEqual(UiString("te\uff012").slice(1, 3), "e");
assert.deepEqual(UiString("te\uff012").slice(2, 3), "");
assert.deepEqual(UiString("te\uff012").slice(2, 4), "\uff01");
assert.deepEqual(UiString("te\uff012").slice(3, 4), "");
assert.deepEqual(UiString("te\uff012").slice(4, 5), "2");
assert.deepEqual(UiString("1\uff01\uff022").slice(1, 2), "");
assert.deepEqual(UiString("1\uff01\uff022").slice(1, 3), "\uff01");
assert.deepEqual(UiString("1\uff01\uff022").slice(1, 4), "\uff01");
assert.deepEqual(UiString("1\uff01\uff022").slice(1, 5), "\uff01\uff02");
assert.deepEqual(UiString("1\uff01\uff022").slice(2, 3), "");
assert.deepEqual(UiString("1\uff01\uff022").slice(2, 4), "\uff01");
assert.deepEqual(UiString("1\uff01\uff022").slice(2, 5), "\uff01");
assert.deepEqual(UiString("1\uff01\uff022").slice(3, 5), "\uff02");
});

it("should return current str if same width when ensureWidth", () => {
//given
const str = "test";

//when & then
assert.deepEqual(UiString(str).ensureWidth(4, " "), str);
});

it("should pad to width if > strWidth when ensureWidth", () => {
//when & then
assert.deepEqual(UiString("Валютный").ensureWidth(8, " "), "Валютный");
assert.deepEqual(UiString("Валютный").ensureWidth(9, " "), "Валютный ");
assert.deepEqual(UiString("Валютный").ensureWidth(10, " "), "Валютный ");
});

it("should cut to width if < strWidth when ensureWidth", () => {
//when & then
assert.deepEqual(UiString("Валютный").ensureWidth(7, " "), "Валютны");
assert.deepEqual(UiString("Валютный").ensureWidth(8, " "), "Валютный");
assert.deepEqual(UiString("Валютный2").ensureWidth(8, " "), "Валютный");
assert.deepEqual(UiString("\uD800\uDC002").ensureWidth(1, " "), " ");
assert.deepEqual(UiString("\uD83C\uDF31-").ensureWidth(1, " "), " ");
assert.deepEqual(
UiString("\uD83C\uDF31-").ensureWidth(2, " "),
"\uD83C\uDF31"
);
});

it("should cut and pad to width if at double-width char when ensureWidth", () => {
//given
const str = "te\uff01t";
assert.deepEqual(str.length, 4);
assert.deepEqual(UiString(str).strWidth(), 5);

//when & then
assert.deepEqual(UiString(str).ensureWidth(6, " "), "te\uff01t ");
assert.deepEqual(UiString(str).ensureWidth(5, " "), "te\uff01t");
assert.deepEqual(UiString(str).ensureWidth(4, " "), "te\uff01");
assert.deepEqual(UiString(str).ensureWidth(3, " "), "te ");
});
});
1 change: 1 addition & 0 deletions ui/test/all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ await import("./Button.test.mjs");
await import("./ButtonsPanel.test.mjs");
await import("./TextLine.test.mjs");
await import("./UI.test.mjs");
await import("./UiString.test.mjs");
await import("./WithSize.test.mjs");

await import("./app/AppRoot.test.mjs");
Expand Down

0 comments on commit f07c939

Please sign in to comment.