@nan0web/ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.datasets/README.dataset.jsonl +12 -0
- package/.editorconfig +20 -0
- package/CONTRIBUTING.md +42 -0
- package/LICENSE +15 -0
- package/README.md +238 -0
- package/docs/uk/README.md +240 -0
- package/package.json +64 -0
- package/playground/User.js +52 -0
- package/playground/currency.exchange.js +48 -0
- package/playground/i18n/index.js +21 -0
- package/playground/i18n/uk.js +53 -0
- package/playground/language.form.js +25 -0
- package/playground/main.js +72 -0
- package/playground/registration.form.js +58 -0
- package/playground/topup.telephone.js +62 -0
- package/src/App/Command/Options.js +78 -0
- package/src/App/Command/index.js +9 -0
- package/src/App/Core/CoreApp.js +129 -0
- package/src/App/Core/UI.js +116 -0
- package/src/App/Core/Widget.js +67 -0
- package/src/App/Core/index.js +11 -0
- package/src/App/Scenario.js +45 -0
- package/src/App/User/Command/Message.js +44 -0
- package/src/App/User/Command/Options.js +48 -0
- package/src/App/User/Command/index.js +11 -0
- package/src/App/User/UserApp.js +73 -0
- package/src/App/User/UserApp.test.js +56 -0
- package/src/App/User/UserUI.js +20 -0
- package/src/App/User/UserUI.test.js +51 -0
- package/src/App/User/index.js +15 -0
- package/src/App/index.js +22 -0
- package/src/Component/Process/Input.js +70 -0
- package/src/Component/Process/Process.js +26 -0
- package/src/Component/Process/index.js +5 -0
- package/src/Component/Welcome/Input.js +50 -0
- package/src/Component/Welcome/Welcome.js +26 -0
- package/src/Component/Welcome/index.js +5 -0
- package/src/Component/index.js +9 -0
- package/src/Frame/Frame.js +591 -0
- package/src/Frame/Frame.test.js +429 -0
- package/src/Frame/Props.js +102 -0
- package/src/Locale.js +119 -0
- package/src/Model/User/User.js +56 -0
- package/src/Model/index.js +7 -0
- package/src/README.md.js +371 -0
- package/src/StdIn.js +111 -0
- package/src/StdOut.js +99 -0
- package/src/View/RenderOptions.js +48 -0
- package/src/View/View.js +289 -0
- package/src/View/View.test.js +77 -0
- package/src/core/Form/Form.js +289 -0
- package/src/core/Form/Form.test.js +116 -0
- package/src/core/Form/Input.js +116 -0
- package/src/core/Form/Input.test.js +58 -0
- package/src/core/Form/Message.js +86 -0
- package/src/core/Form/Message.test.js +54 -0
- package/src/core/Form/index.js +11 -0
- package/src/core/InputAdapter.js +41 -0
- package/src/core/InputAdapter.test.js +35 -0
- package/src/core/Message/InputMessage.js +119 -0
- package/src/core/Message/InputMessage.test.js +45 -0
- package/src/core/Message/Message.js +77 -0
- package/src/core/Message/Message.test.js +58 -0
- package/src/core/Message/OutputMessage.js +143 -0
- package/src/core/Message/OutputMessage.test.js +61 -0
- package/src/core/Message/index.js +7 -0
- package/src/core/OutputAdapter.js +50 -0
- package/src/core/OutputAdapter.test.js +35 -0
- package/src/core/Stream.js +71 -0
- package/src/core/Stream.test.js +78 -0
- package/src/core/StreamEntry.js +59 -0
- package/src/core/index.js +13 -0
- package/src/functions.js +38 -0
- package/src/index.js +34 -0
- package/src/index.test.js +14 -0
- package/src/models/SimpleUser.js +18 -0
- package/stories/App/AppView.js +15 -0
- package/stories/App/AppView.test.js +22 -0
- package/stories/App/RenderOptions.js +14 -0
- package/stories/nodejs/interface.test.js +27 -0
- package/system.md +187 -0
- package/system1.md +137 -0
- package/task.md +181 -0
- package/tsconfig.json +23 -0
- package/types/App/Command/Options.d.ts +46 -0
- package/types/App/Command/index.d.ts +8 -0
- package/types/App/Core/CoreApp.d.ts +70 -0
- package/types/App/Core/UI.d.ts +49 -0
- package/types/App/Core/Widget.d.ts +40 -0
- package/types/App/Core/index.d.ts +10 -0
- package/types/App/Scenario.d.ts +26 -0
- package/types/App/User/Command/Message.d.ts +30 -0
- package/types/App/User/Command/Options.d.ts +27 -0
- package/types/App/User/Command/index.d.ts +8 -0
- package/types/App/User/UserApp.d.ts +31 -0
- package/types/App/User/UserUI.d.ts +18 -0
- package/types/App/User/index.d.ts +12 -0
- package/types/App/index.d.ts +14 -0
- package/types/Component/Process/Input.d.ts +48 -0
- package/types/Component/Process/Process.d.ts +13 -0
- package/types/Component/Process/index.d.ts +4 -0
- package/types/Component/Welcome/Input.d.ts +34 -0
- package/types/Component/Welcome/Welcome.d.ts +13 -0
- package/types/Component/Welcome/index.d.ts +4 -0
- package/types/Component/index.d.ts +8 -0
- package/types/Frame/Frame.d.ts +186 -0
- package/types/Frame/Props.d.ts +77 -0
- package/types/Locale.d.ts +55 -0
- package/types/Model/User/User.d.ts +36 -0
- package/types/Model/index.d.ts +6 -0
- package/types/StdIn.d.ts +62 -0
- package/types/StdOut.d.ts +52 -0
- package/types/View/RenderOptions.d.ts +29 -0
- package/types/View/View.d.ts +115 -0
- package/types/core/Form/Form.d.ts +123 -0
- package/types/core/Form/Input.d.ts +69 -0
- package/types/core/Form/Message.d.ts +28 -0
- package/types/core/Form/index.d.ts +5 -0
- package/types/core/InputAdapter.d.ts +28 -0
- package/types/core/Message/InputMessage.d.ts +71 -0
- package/types/core/Message/Message.d.ts +50 -0
- package/types/core/Message/OutputMessage.d.ts +53 -0
- package/types/core/Message/index.d.ts +5 -0
- package/types/core/OutputAdapter.d.ts +33 -0
- package/types/core/Stream.d.ts +27 -0
- package/types/core/StreamEntry.d.ts +45 -0
- package/types/core/index.d.ts +9 -0
- package/types/functions.d.ts +3 -0
- package/types/index.d.ts +20 -0
- package/types/models/SimpleUser.d.ts +21 -0
- package/vitest.config.js +26 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { describe, it } from "node:test"
|
|
2
|
+
import { strict as assert } from "node:assert"
|
|
3
|
+
import { empty, notEmpty } from "@nan0web/types"
|
|
4
|
+
import Frame from "./Frame.js"
|
|
5
|
+
import stringWidth from "string-width"
|
|
6
|
+
|
|
7
|
+
describe("Frame", () => {
|
|
8
|
+
it("should create empty Frame", () => {
|
|
9
|
+
const frame = new Frame()
|
|
10
|
+
assert.ok(empty(frame))
|
|
11
|
+
})
|
|
12
|
+
it("should create non-empty Frame", () => {
|
|
13
|
+
const frame = Frame.from(["Non", "empty"])
|
|
14
|
+
assert.ok(notEmpty(frame))
|
|
15
|
+
})
|
|
16
|
+
it("should print empty zero and false", () => {
|
|
17
|
+
const input = [
|
|
18
|
+
[0, "0", false, "false"],
|
|
19
|
+
[undefined, null, "", {}]
|
|
20
|
+
]
|
|
21
|
+
const frame = new Frame({ value: input, width: 144, height: 33 })
|
|
22
|
+
assert.ok(notEmpty(frame))
|
|
23
|
+
const rows = input.map(row => row.map(String))
|
|
24
|
+
assert.deepStrictEqual(frame.value, rows)
|
|
25
|
+
frame.render()
|
|
26
|
+
assert.equal(
|
|
27
|
+
frame.imprint,
|
|
28
|
+
rows.map(
|
|
29
|
+
row => row.join("")
|
|
30
|
+
).map(
|
|
31
|
+
row => row + " ".repeat(144 - stringWidth(row))
|
|
32
|
+
).join("\n")
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
it("should print welcome", () => {
|
|
36
|
+
const input = [
|
|
37
|
+
["Welcome", " ", "World", "!"],
|
|
38
|
+
["What can we do today great?"],
|
|
39
|
+
]
|
|
40
|
+
const rows = input.map(
|
|
41
|
+
row => row.join("")
|
|
42
|
+
).map(
|
|
43
|
+
row => row + " ".repeat(144 - stringWidth(row))
|
|
44
|
+
)
|
|
45
|
+
const frame = new Frame({ value: input, width: 144, height: 33 })
|
|
46
|
+
frame.render()
|
|
47
|
+
assert.equal(frame.imprint, rows.join("\n"))
|
|
48
|
+
})
|
|
49
|
+
it("should transform value", () => {
|
|
50
|
+
const frame = Frame.from(["Welcome"])
|
|
51
|
+
const t = v => "Вітання"
|
|
52
|
+
const transformed = frame.transform(t)
|
|
53
|
+
assert.deepStrictEqual(transformed.value, [["Вітання"]])
|
|
54
|
+
})
|
|
55
|
+
it("should print special utf characters and fit the width", () => {
|
|
56
|
+
const input = [
|
|
57
|
+
["你好", "世界"],
|
|
58
|
+
["Привіт", "Світ"],
|
|
59
|
+
["こんにちは", "世界"],
|
|
60
|
+
]
|
|
61
|
+
const frame = Frame.from(input)
|
|
62
|
+
frame.render()
|
|
63
|
+
const imprintLines = frame.imprint.split("\n")
|
|
64
|
+
imprintLines.forEach(line => {
|
|
65
|
+
assert.ok(stringWidth(line) <= 144)
|
|
66
|
+
})
|
|
67
|
+
assert.deepStrictEqual(frame.value, input)
|
|
68
|
+
})
|
|
69
|
+
it("should render table with padding", () => {
|
|
70
|
+
const rows = [
|
|
71
|
+
["gpt-4.1", 1_047_576],
|
|
72
|
+
["gpt-4o", 128_000],
|
|
73
|
+
]
|
|
74
|
+
const table = Frame.table({ padding: 3 })(rows)
|
|
75
|
+
assert.deepStrictEqual(table, [
|
|
76
|
+
["gpt-4.1 ", "1047576"],
|
|
77
|
+
["gpt-4o ", "128000"],
|
|
78
|
+
])
|
|
79
|
+
})
|
|
80
|
+
it("should render with replace method and fill spaces", () => {
|
|
81
|
+
const input = [["Test"]]
|
|
82
|
+
const frame = new Frame({ value: input, width: 10, height: 3 })
|
|
83
|
+
const output = frame.render({ method: Frame.RenderMethod.REPLACE }).split("\n")
|
|
84
|
+
assert.equal(output.length, 3)
|
|
85
|
+
assert.equal(output[0].length - `\x1b[0;0H`.length, 10)
|
|
86
|
+
assert.equal(output[1].length, 10)
|
|
87
|
+
assert.equal(output[2].length, 10)
|
|
88
|
+
})
|
|
89
|
+
it("should render with append method over previous frame", () => {
|
|
90
|
+
const input = [["Append"]]
|
|
91
|
+
const frame = new Frame({ value: input, width: 10, height: 3 })
|
|
92
|
+
const output = frame.render({ method: "append" }).split("\n")
|
|
93
|
+
assert.ok(output.length <= 3)
|
|
94
|
+
assert.ok(output[0].length <= 10)
|
|
95
|
+
})
|
|
96
|
+
it("should render visible method", () => {
|
|
97
|
+
const input = [["Visible"]]
|
|
98
|
+
const frame = new Frame({ value: input, width: 10, height: 3 })
|
|
99
|
+
const output = frame.render({ method: "visible" }).split("\n")
|
|
100
|
+
assert.ok(output.length <= 3)
|
|
101
|
+
})
|
|
102
|
+
it("should handle cell options with style objects", () => {
|
|
103
|
+
const input = [
|
|
104
|
+
["<b>Hello</b>", "<i>World</i>"],
|
|
105
|
+
["<fg=red>Red</>", "<bg=#00ff00>Green background</>"]
|
|
106
|
+
]
|
|
107
|
+
const frame = new Frame({ value: input, width: 20, height: 4 })
|
|
108
|
+
const output = frame.render()
|
|
109
|
+
assert.ok(output.split("\n").length <= 4)
|
|
110
|
+
})
|
|
111
|
+
it("should handle row options with style objects", () => {
|
|
112
|
+
const input = [
|
|
113
|
+
["Name", "Age"],
|
|
114
|
+
["John", 30],
|
|
115
|
+
["<u>Underlined</u>", "<s>Strikethrough</s>", { color: "blue" }]
|
|
116
|
+
]
|
|
117
|
+
const frame = new Frame({ value: input, width: 30, height: 4 })
|
|
118
|
+
const output = frame.render()
|
|
119
|
+
assert.ok(output.split("\n").length <= 4)
|
|
120
|
+
})
|
|
121
|
+
it("should handle frame options set by method with XML tags", () => {
|
|
122
|
+
const input = [
|
|
123
|
+
["<b>Bold</b>", "<i>Italic</i>"],
|
|
124
|
+
["<fg=red>Red</fg>", "<bg=#0000ff>Blue bg</bg>"]
|
|
125
|
+
]
|
|
126
|
+
const frame = new Frame({ value: input, width: 20, height: 4 })
|
|
127
|
+
const output = frame.render()
|
|
128
|
+
assert.ok(output.split("\n").length <= 4)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("should render correctly with different window sizes", () => {
|
|
132
|
+
const input = [
|
|
133
|
+
["Line 1: Hello World!"],
|
|
134
|
+
["Line 2: Another line"],
|
|
135
|
+
["Line 3: Yet another line"],
|
|
136
|
+
["Line 4: More text here"],
|
|
137
|
+
["Line 5: Last line"]
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
const sizes = [
|
|
141
|
+
{ width: 10, height: 2 },
|
|
142
|
+
{ width: 20, height: 3 },
|
|
143
|
+
{ width: 50, height: 5 },
|
|
144
|
+
{ width: 144, height: 33 },
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
sizes.forEach(({ width, height }) => {
|
|
148
|
+
const frame = new Frame({ value: input, width, height })
|
|
149
|
+
const output = frame.render({ method: Frame.RenderMethod.APPEND })
|
|
150
|
+
const lines = output.split("\n")
|
|
151
|
+
assert.ok(lines.length <= height)
|
|
152
|
+
lines.forEach(line => {
|
|
153
|
+
assert.ok(stringWidth(line) <= width)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Additional tests for BOF, BOL in different positions with REPLACE, VISIBLE, APPEND
|
|
159
|
+
|
|
160
|
+
it("should handle BOF at start with REPLACE method", () => {
|
|
161
|
+
const input = [
|
|
162
|
+
Frame.BOF,
|
|
163
|
+
["Line 1"],
|
|
164
|
+
["Line 2"]
|
|
165
|
+
]
|
|
166
|
+
const frame = new Frame({ value: input, width: 10, height: 4 })
|
|
167
|
+
const output = frame.render({ method: Frame.RenderMethod.REPLACE })
|
|
168
|
+
assert.ok(output.startsWith(Frame.BOF))
|
|
169
|
+
const lines = output.split("\n")
|
|
170
|
+
assert.equal(lines.length, 4)
|
|
171
|
+
assert.match(lines[1], /^\s*$/) // empty row
|
|
172
|
+
assert.deepStrictEqual(lines.slice(-2), ["Line 1 ", "Line 2 "])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it("should handle BOF at end with REPLACE method", () => {
|
|
176
|
+
const input = [
|
|
177
|
+
["Line 1"],
|
|
178
|
+
["Line 2"],
|
|
179
|
+
Frame.BOF
|
|
180
|
+
]
|
|
181
|
+
const frame = new Frame({ value: input, width: 10, height: 4 })
|
|
182
|
+
const output = frame.render({ method: Frame.RenderMethod.REPLACE })
|
|
183
|
+
assert.ok(output.endsWith(Frame.BOF))
|
|
184
|
+
const lines = output.split("\n")
|
|
185
|
+
assert.equal(lines.length, 4)
|
|
186
|
+
assert.deepStrictEqual(lines.slice(0, 2), [
|
|
187
|
+
"Line 1 ",
|
|
188
|
+
"Line 2 "
|
|
189
|
+
])
|
|
190
|
+
assert.match(lines[2], /^\s*$/) // empty row
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it("should handle BOF at start with APPEND method", () => {
|
|
194
|
+
const input = [
|
|
195
|
+
Frame.BOF,
|
|
196
|
+
["Line 1"],
|
|
197
|
+
["Line 2"]
|
|
198
|
+
]
|
|
199
|
+
const frame = new Frame({ value: input, width: 10, height: 4 })
|
|
200
|
+
const output = frame.render({ method: Frame.RenderMethod.APPEND })
|
|
201
|
+
assert.ok(output.startsWith(Frame.BOF))
|
|
202
|
+
const lines = output.split("\n")
|
|
203
|
+
assert.ok(lines.length <= 4)
|
|
204
|
+
assert.deepStrictEqual(lines.slice(-2), ["Line 1 ", "Line 2 "])
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it("should handle BOF at end with APPEND method", () => {
|
|
208
|
+
const input = [
|
|
209
|
+
["Line 1"],
|
|
210
|
+
["Line 2"],
|
|
211
|
+
Frame.BOF
|
|
212
|
+
]
|
|
213
|
+
const frame = new Frame({ value: input, width: 10, height: 4 })
|
|
214
|
+
const output = frame.render({ method: Frame.RenderMethod.APPEND })
|
|
215
|
+
assert.ok(output.endsWith(Frame.BOF))
|
|
216
|
+
const lines = output.split("\n")
|
|
217
|
+
assert.equal(lines.length, 4)
|
|
218
|
+
assert.deepStrictEqual(lines.slice(1, 3), ["Line 2 ", ""])
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("should handle BOF at start with VISIBLE method", () => {
|
|
222
|
+
const input = [
|
|
223
|
+
Frame.BOF,
|
|
224
|
+
["Line 1"],
|
|
225
|
+
["Line 2"]
|
|
226
|
+
]
|
|
227
|
+
const frame = new Frame({ value: input, width: 10, height: 4 })
|
|
228
|
+
const output = frame.render({ method: Frame.RenderMethod.VISIBLE })
|
|
229
|
+
assert.ok(output.startsWith(`\x1b[1A`))
|
|
230
|
+
const lines = output.split("\n")
|
|
231
|
+
assert.ok(lines.length <= 2)
|
|
232
|
+
assert.deepStrictEqual(lines, [
|
|
233
|
+
Frame.cursorUp() + Frame.CLEAR_LINE + "\r" + "Line 1",
|
|
234
|
+
Frame.CLEAR_LINE + "\r" + "Line 2"
|
|
235
|
+
])
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it("should handle BOF at end with VISIBLE method", () => {
|
|
239
|
+
const input = [
|
|
240
|
+
["Line 1"],
|
|
241
|
+
["Line 2"],
|
|
242
|
+
Frame.BOF
|
|
243
|
+
]
|
|
244
|
+
const frame = new Frame({ value: input, width: 10, height: 4 })
|
|
245
|
+
const output = frame.render({ method: Frame.RenderMethod.VISIBLE })
|
|
246
|
+
assert.ok(output.startsWith(Frame.CLEAR_LINE + "\r"))
|
|
247
|
+
const lines = output.split("\n")
|
|
248
|
+
assert.ok(lines.length <= 2)
|
|
249
|
+
assert.deepStrictEqual(lines, [
|
|
250
|
+
Frame.CLEAR_LINE + "\r" + "Line 1",
|
|
251
|
+
Frame.CLEAR_LINE + "\r" + "Line 2" + Frame.cursorUp(1)
|
|
252
|
+
])
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it("should handle BOL in lines with REPLACE method", () => {
|
|
256
|
+
const input = [
|
|
257
|
+
["Line 1" + Frame.BOL],
|
|
258
|
+
["Line 2"]
|
|
259
|
+
]
|
|
260
|
+
const frame = new Frame({ value: input, width: 10, height: 3 })
|
|
261
|
+
const output = frame.render({ method: Frame.RenderMethod.REPLACE })
|
|
262
|
+
const lines = output.split("\n")
|
|
263
|
+
assert.ok(lines.some(line => line.includes(Frame.BOL)))
|
|
264
|
+
assert.equal(lines.length, 3)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it("should handle BOL in lines with APPEND method", () => {
|
|
268
|
+
const input = [
|
|
269
|
+
["Line 1" + Frame.BOL],
|
|
270
|
+
["Line 2"]
|
|
271
|
+
]
|
|
272
|
+
const frame = new Frame({ value: input, width: 10, height: 3 })
|
|
273
|
+
const output = frame.render({ method: Frame.RenderMethod.APPEND })
|
|
274
|
+
const lines = output.split("\n")
|
|
275
|
+
assert.ok(lines.some(line => line.includes(Frame.BOL)))
|
|
276
|
+
assert.ok(lines.length <= 3)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it("should handle BOL in lines with VISIBLE method", () => {
|
|
280
|
+
const input = [
|
|
281
|
+
["Line 1" + Frame.BOL],
|
|
282
|
+
["Line 2"]
|
|
283
|
+
]
|
|
284
|
+
const frame = new Frame({ value: input, width: 10, height: 3 })
|
|
285
|
+
const output = frame.render({ method: Frame.RenderMethod.VISIBLE })
|
|
286
|
+
const lines = output.split("\n")
|
|
287
|
+
assert.ok(lines.some(line => line.includes(Frame.BOL)))
|
|
288
|
+
assert.ok(lines.length <= 3)
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
describe("Frame.RenderMethod", () => {
|
|
293
|
+
const table = Frame.table({
|
|
294
|
+
aligns: ["r"],
|
|
295
|
+
padding: 2,
|
|
296
|
+
})([
|
|
297
|
+
["Usage: node memory-usage.js", "", ""],
|
|
298
|
+
["[process_name]", "", "Name of the process to check memory usage for"],
|
|
299
|
+
["-n number_of_processes", "", "Number of top memory consumers to show (default: 9)"],
|
|
300
|
+
["--help", "", "Show this help message"],
|
|
301
|
+
])
|
|
302
|
+
const frame = new Frame({
|
|
303
|
+
value: [...table, ""],
|
|
304
|
+
renderMethod: Frame.RenderMethod.REPLACE,
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it("should render with REPLACE method", () => {
|
|
308
|
+
const output = frame.render({
|
|
309
|
+
method: Frame.RenderMethod.REPLACE,
|
|
310
|
+
})
|
|
311
|
+
assert.equal(typeof output, "string")
|
|
312
|
+
const lines = output.split("\n")
|
|
313
|
+
assert.ok(lines.length > 0)
|
|
314
|
+
lines.forEach(line => {
|
|
315
|
+
assert.ok(line.length <= (frame.width < 0 ? 144 : frame.width))
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it("should render with APPEND method", () => {
|
|
320
|
+
const output = frame.render({ method: Frame.RenderMethod.APPEND })
|
|
321
|
+
assert.equal(typeof output, "string")
|
|
322
|
+
const lines = output.split("\n")
|
|
323
|
+
const height = frame.height < 0 ? 10 : frame.height
|
|
324
|
+
const width = frame.width < 0 ? 144 : frame.width
|
|
325
|
+
assert.ok(lines.length <= height)
|
|
326
|
+
lines.forEach(line => {
|
|
327
|
+
assert.ok(line.length <= width)
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it("should render with VISIBLE method", () => {
|
|
332
|
+
const output = frame.render({ method: Frame.RenderMethod.VISIBLE })
|
|
333
|
+
assert.equal(typeof output, "string")
|
|
334
|
+
const lines = output.split("\n")
|
|
335
|
+
const height = frame.height < 0 ? 10 : frame.height
|
|
336
|
+
const width = frame.width < 0 ? 144 : frame.width
|
|
337
|
+
assert.ok(lines.length <= height)
|
|
338
|
+
lines.forEach(line => {
|
|
339
|
+
assert.ok(line.length <= width)
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
describe("Frame (3rd party deprecations)", () => {
|
|
345
|
+
it("should detect extra lines above BOF and allow for offset rendering", () => {
|
|
346
|
+
// Simulate a terminal with 2 extra lines above (e.g., from warnings)
|
|
347
|
+
const extraLines = 2
|
|
348
|
+
const selectBox = [
|
|
349
|
+
Frame.BOF,
|
|
350
|
+
["Select a command to run"],
|
|
351
|
+
["[+]draw"],
|
|
352
|
+
[" - send"],
|
|
353
|
+
[" - stats"],
|
|
354
|
+
[" - analyze"],
|
|
355
|
+
[" - extract"]
|
|
356
|
+
]
|
|
357
|
+
const frame = new Frame({ value: selectBox, width: 30, height: 8 })
|
|
358
|
+
const output = frame.render({ method: Frame.RenderMethod.REPLACE })
|
|
359
|
+
// The output should start with BOF and have the select box content
|
|
360
|
+
assert.ok(output.startsWith(Frame.BOF))
|
|
361
|
+
const lines = output.split("\n")
|
|
362
|
+
// Simulate that the select box is rendered at the top, but if there are extra lines above,
|
|
363
|
+
// the select box will be shifted down by `extraLines`
|
|
364
|
+
// To check for this, we can simulate a "screen" with extra lines and see where the select box appears
|
|
365
|
+
const screen = [
|
|
366
|
+
"DeprecationWarning: ...", // extra line 1
|
|
367
|
+
"Some other warning...", // extra line 2
|
|
368
|
+
...lines
|
|
369
|
+
]
|
|
370
|
+
// The select box should appear at index `extraLines` (after the warnings)
|
|
371
|
+
assert.ok(screen[extraLines + 2].includes("Select a command to run"))
|
|
372
|
+
assert.ok(screen[extraLines + 3].includes("[+]draw"))
|
|
373
|
+
assert.ok(screen[extraLines + 4].includes("- send"))
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it("should allow for offset rendering by prepending empty lines before BOF", () => {
|
|
377
|
+
// If you want to compensate for extra lines, you can prepend empty lines before BOF
|
|
378
|
+
const extraLines = 2
|
|
379
|
+
const selectBox = [
|
|
380
|
+
"",
|
|
381
|
+
"",
|
|
382
|
+
Frame.BOF,
|
|
383
|
+
["Select a command to run"],
|
|
384
|
+
["[+]draw"],
|
|
385
|
+
[" - send"],
|
|
386
|
+
[" - stats"],
|
|
387
|
+
[" - analyze"],
|
|
388
|
+
[" - extract"]
|
|
389
|
+
]
|
|
390
|
+
const frame = new Frame({ value: selectBox, width: 30, height: 10 })
|
|
391
|
+
const output = frame.render({ method: Frame.RenderMethod.REPLACE })
|
|
392
|
+
const lines = output.split("\n")
|
|
393
|
+
// The select box should now appear after the empty lines
|
|
394
|
+
assert.equal(lines[4], "Select a command to run".padEnd(30))
|
|
395
|
+
assert.equal(lines[5], "[+]draw".padEnd(30))
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it("should render select box correctly even with extra lines above (simulate terminal shift)", () => {
|
|
399
|
+
// Simulate a terminal with 2 extra lines above (e.g., from warnings)
|
|
400
|
+
const extraLines = 2
|
|
401
|
+
const selectBox = [
|
|
402
|
+
Frame.BOF,
|
|
403
|
+
["Select a command to run"],
|
|
404
|
+
["[+]draw"],
|
|
405
|
+
[" - send"],
|
|
406
|
+
[" - stats"],
|
|
407
|
+
[" - analyze"],
|
|
408
|
+
[" - extract"]
|
|
409
|
+
]
|
|
410
|
+
const frame = new Frame({ value: selectBox, width: 30, height: 8 })
|
|
411
|
+
const output = frame.render({ method: Frame.RenderMethod.REPLACE })
|
|
412
|
+
const lines = output.split("\n")
|
|
413
|
+
// Simulate a "screen" with extra lines above
|
|
414
|
+
const screen = [
|
|
415
|
+
"DeprecationWarning: ...",
|
|
416
|
+
"Some other warning...",
|
|
417
|
+
...lines
|
|
418
|
+
]
|
|
419
|
+
// The select box should still be visible at the correct offset
|
|
420
|
+
assert.equal(screen[extraLines].trim(), `\x1b[0;0H`)
|
|
421
|
+
assert.equal(screen[extraLines + 1].trim(), "")
|
|
422
|
+
assert.equal(screen[extraLines + 2].trim(), "Select a command to run")
|
|
423
|
+
assert.equal(screen[extraLines + 3].trim(), "[+]draw")
|
|
424
|
+
assert.equal(screen[extraLines + 4].trim(), "- send")
|
|
425
|
+
assert.equal(screen[extraLines + 5].trim(), "- stats")
|
|
426
|
+
assert.equal(screen[extraLines + 6].trim(), "- analyze")
|
|
427
|
+
assert.equal(screen[extraLines + 7].trim(), "- extract")
|
|
428
|
+
})
|
|
429
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { typeOf, ObjectWithAlias } from "@nan0web/types"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents default styling properties for Frame rendering.
|
|
5
|
+
* Every tag must be a separate value in the array of rows/columns.
|
|
6
|
+
* If you want to apply the same props to multiple values, you can use an array of values.
|
|
7
|
+
* If you want to apply different props to multiple values, you can use an object with the props.
|
|
8
|
+
* If you want to apply props to a single value, you can use a string with the props in XML format.
|
|
9
|
+
* Parser checks every atom for its beginning and end and if it's a tag, it applies the props to the value.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const defaultProps = new FrameProps({
|
|
13
|
+
* color: "red",
|
|
14
|
+
* bgColor: "blue",
|
|
15
|
+
* bold: true,
|
|
16
|
+
* italic: true,
|
|
17
|
+
* underline: true,
|
|
18
|
+
* strikethrough: true,
|
|
19
|
+
* })
|
|
20
|
+
* or by aliases:
|
|
21
|
+
* const defaultProps = new FrameProps({
|
|
22
|
+
* fg: "red",
|
|
23
|
+
* bg: "blue",
|
|
24
|
+
* b: true,
|
|
25
|
+
* i: true,
|
|
26
|
+
* u: true,
|
|
27
|
+
* s: true,
|
|
28
|
+
* })
|
|
29
|
+
* from an array of strings:
|
|
30
|
+
* const rows = [
|
|
31
|
+
* ["Hello", "World"],
|
|
32
|
+
* ["<fg=red>Hello</>", "<bg=blue>World</>"],
|
|
33
|
+
* ["<b>Hello</b>", "<i>World</i>"],
|
|
34
|
+
* ["<u>Hello</u>", "<s>World</s>"],
|
|
35
|
+
* ["<b fg=red>Hello</b>", "<i bg=blue>World</i>"],
|
|
36
|
+
* ["<b i>Hello</b>", "<i b>World</i>"],
|
|
37
|
+
* ["<b i s>Some</b>", ["thing", {b: true, i: true, s: true}]],
|
|
38
|
+
* [["Hello", "World", {b: true}]],
|
|
39
|
+
* ]
|
|
40
|
+
* const defaultProps = new FrameProps(rows)
|
|
41
|
+
*/
|
|
42
|
+
class FrameProps extends ObjectWithAlias {
|
|
43
|
+
/**
|
|
44
|
+
* Property aliases for shorthand notation.
|
|
45
|
+
* @type {Record<string, string>}
|
|
46
|
+
*/
|
|
47
|
+
static ALIAS = {
|
|
48
|
+
fg: "color",
|
|
49
|
+
bg: "bgColor",
|
|
50
|
+
b: "bold",
|
|
51
|
+
i: "italic",
|
|
52
|
+
u: "underline",
|
|
53
|
+
s: "strikethrough",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @type {string} Text color */
|
|
57
|
+
color = ""
|
|
58
|
+
|
|
59
|
+
/** @type {string} Background color */
|
|
60
|
+
bgColor = ""
|
|
61
|
+
|
|
62
|
+
/** @type {boolean} Bold text flag */
|
|
63
|
+
bold = false
|
|
64
|
+
|
|
65
|
+
/** @type {boolean} Italic text flag */
|
|
66
|
+
italic = false
|
|
67
|
+
|
|
68
|
+
/** @type {boolean} Underline text flag */
|
|
69
|
+
underline = false
|
|
70
|
+
|
|
71
|
+
/** @type {boolean} Strikethrough text flag */
|
|
72
|
+
strikethrough = false
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {object} props - Frame properties
|
|
76
|
+
* @param {string} [props.color=""] - Text color
|
|
77
|
+
* @param {string} [props.bgColor=""] - Background color
|
|
78
|
+
* @param {boolean} [props.bold=false] - Bold text flag
|
|
79
|
+
* @param {boolean} [props.italic=false] - Italic text flag
|
|
80
|
+
* @param {boolean} [props.underline=false] - Underline text flag
|
|
81
|
+
* @param {boolean} [props.strikethrough=false] - Strikethrough text flag
|
|
82
|
+
*/
|
|
83
|
+
constructor(props = {}) {
|
|
84
|
+
super()
|
|
85
|
+
const {
|
|
86
|
+
color = "",
|
|
87
|
+
bgColor = "",
|
|
88
|
+
bold = false,
|
|
89
|
+
italic = false,
|
|
90
|
+
underline = false,
|
|
91
|
+
strikethrough = false,
|
|
92
|
+
} = props
|
|
93
|
+
this.color = color
|
|
94
|
+
this.bgColor = bgColor
|
|
95
|
+
this.bold = bold
|
|
96
|
+
this.italic = italic
|
|
97
|
+
this.underline = underline
|
|
98
|
+
this.strikethrough = strikethrough
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default FrameProps
|
package/src/Locale.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { typeOf, notEmpty } from "@nan0web/types"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handles locale-specific formatting for different data types.
|
|
5
|
+
*/
|
|
6
|
+
export default class Locale {
|
|
7
|
+
/** @type {string} Language locale */
|
|
8
|
+
lang
|
|
9
|
+
|
|
10
|
+
/** @type {string} Collation locale */
|
|
11
|
+
collate
|
|
12
|
+
|
|
13
|
+
/** @type {string} Character type locale */
|
|
14
|
+
ctype
|
|
15
|
+
|
|
16
|
+
/** @type {string} Messages locale */
|
|
17
|
+
messages
|
|
18
|
+
|
|
19
|
+
/** @type {string} Monetary locale */
|
|
20
|
+
monetary
|
|
21
|
+
|
|
22
|
+
/** @type {string} Numeric locale */
|
|
23
|
+
numeric
|
|
24
|
+
|
|
25
|
+
/** @type {string} Time locale */
|
|
26
|
+
time
|
|
27
|
+
|
|
28
|
+
/** @type {string} General locale fallback */
|
|
29
|
+
all
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a new Locale instance.
|
|
33
|
+
* @param {object} props - Locale properties or all locale string
|
|
34
|
+
* @param {string} [props.lang=""] - Language locale
|
|
35
|
+
* @param {string} [props.collate=""] - Collation locale
|
|
36
|
+
* @param {string} [props.ctype=""] - Character type locale
|
|
37
|
+
* @param {string} [props.messages=""] - Messages locale
|
|
38
|
+
* @param {string} [props.monetary=""] - Monetary locale
|
|
39
|
+
* @param {string} [props.numeric=""] - Numeric locale
|
|
40
|
+
* @param {string} [props.time=""] - Time locale
|
|
41
|
+
* @param {string} [props.all="uk_UA.UTF-8"] - General locale fallback
|
|
42
|
+
*/
|
|
43
|
+
constructor(props = {}) {
|
|
44
|
+
const {
|
|
45
|
+
lang = "",
|
|
46
|
+
collate = "",
|
|
47
|
+
ctype = "",
|
|
48
|
+
messages = "",
|
|
49
|
+
monetary = "",
|
|
50
|
+
numeric = "",
|
|
51
|
+
time = "",
|
|
52
|
+
all = "uk_UA.UTF-8",
|
|
53
|
+
} = props
|
|
54
|
+
this.lang = lang
|
|
55
|
+
this.collate = collate
|
|
56
|
+
this.ctype = ctype
|
|
57
|
+
this.messages = messages
|
|
58
|
+
this.monetary = monetary
|
|
59
|
+
this.numeric = numeric
|
|
60
|
+
this.time = time
|
|
61
|
+
this.all = all
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Formats values according to locale settings.
|
|
66
|
+
* @param {Function} type - Type constructor (Number, String, etc.)
|
|
67
|
+
* @param {object} options - Formatting options
|
|
68
|
+
* @returns {Function|null} Formatting function or null if unsupported type
|
|
69
|
+
*/
|
|
70
|
+
format(type, options) {
|
|
71
|
+
if (Number === type || typeOf(Number)(type)) {
|
|
72
|
+
/**
|
|
73
|
+
* new (locales?: LocalesArgument, options?: NumberFormatOptions): NumberFormat;
|
|
74
|
+
* (locales?: LocalesArgument, options?: NumberFormatOptions): NumberFormat;
|
|
75
|
+
* supportedLocalesOf(locales: LocalesArgument, options?: NumberFormatOptions): string[];
|
|
76
|
+
*/
|
|
77
|
+
/**
|
|
78
|
+
* localeMatcher?: "lookup" | "best fit" | undefined;
|
|
79
|
+
* style?: NumberFormatOptionsStyle | undefined;
|
|
80
|
+
* currency?: string | undefined;
|
|
81
|
+
* currencyDisplay?: NumberFormatOptionsCurrencyDisplay | undefined;
|
|
82
|
+
* useGrouping?: NumberFormatOptionsUseGrouping | undefined;
|
|
83
|
+
* minimumIntegerDigits?: number | undefined;
|
|
84
|
+
* minimumFractionDigits?: number | undefined;
|
|
85
|
+
* maximumFractionDigits?: number | undefined;
|
|
86
|
+
* minimumSignificantDigits?: number | undefined;
|
|
87
|
+
* maximumSignificantDigits?: number | undefined;
|
|
88
|
+
*/
|
|
89
|
+
const locales = [this.numeric, this.all, this.lang].filter(notEmpty)
|
|
90
|
+
return (value) => {
|
|
91
|
+
return new Intl.NumberFormat(locales, options).format(value)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if ("string" === typeof type) {
|
|
95
|
+
const locales = [this.monetary, this.numeric, this.all, this.lang].filter(notEmpty)
|
|
96
|
+
return (value) => {
|
|
97
|
+
return new Intl.NumberFormat(locales, {
|
|
98
|
+
style: "currency",
|
|
99
|
+
currency: type === "currency" ? options.currency : type,
|
|
100
|
+
...options,
|
|
101
|
+
}).format(value)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {any} input
|
|
109
|
+
* @returns {Locale}
|
|
110
|
+
*/
|
|
111
|
+
static from(input) {
|
|
112
|
+
if (input instanceof Locale) return input
|
|
113
|
+
if ("string" === typeof input) {
|
|
114
|
+
return new Locale({ all: input })
|
|
115
|
+
}
|
|
116
|
+
return new Locale(input)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a user with name and email properties.
|
|
3
|
+
*/
|
|
4
|
+
class User {
|
|
5
|
+
/** @type {string} User name */
|
|
6
|
+
name
|
|
7
|
+
|
|
8
|
+
/** @type {string} User email */
|
|
9
|
+
email
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a new User instance.
|
|
13
|
+
* @param {object} props - User properties or name string
|
|
14
|
+
* @param {string} [props.name=""] - User name
|
|
15
|
+
* @param {string} [props.email=""] - User email
|
|
16
|
+
*/
|
|
17
|
+
constructor(props = {}) {
|
|
18
|
+
const {
|
|
19
|
+
name = "",
|
|
20
|
+
email = "",
|
|
21
|
+
} = props
|
|
22
|
+
this.name = String(name)
|
|
23
|
+
this.email = String(email)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Checks if user data is empty (no name and no email).
|
|
28
|
+
* @returns {boolean} True if both name and email are empty, false otherwise
|
|
29
|
+
*/
|
|
30
|
+
get empty() {
|
|
31
|
+
return !this.name && !this.email
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Converts user to string representation.
|
|
36
|
+
* @returns {string} User name and email (if exists)
|
|
37
|
+
*/
|
|
38
|
+
toString() {
|
|
39
|
+
return [this.name, this.email ? `<${this.email}>` : ""].filter(Boolean).join(" ")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a User instance from the given props.
|
|
44
|
+
* @param {User|object|string} props - The properties to create from
|
|
45
|
+
* @returns {User} A User instance
|
|
46
|
+
*/
|
|
47
|
+
static from(props) {
|
|
48
|
+
if (props instanceof User) return props
|
|
49
|
+
if ("string" === typeof props) {
|
|
50
|
+
return new User({ name: props })
|
|
51
|
+
}
|
|
52
|
+
return new User(props)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default User
|