@holoscript/std 2.1.0 → 3.1.1
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/README.md +197 -0
- package/dist/__tests__/EconomicPrimitives.test.d.ts.map +1 -0
- package/dist/__tests__/EconomicTraits.test.d.ts.map +1 -0
- package/dist/__tests__/SimulationLabPrimitives.test.d.ts.map +1 -0
- package/dist/__tests__/Sprint33.test.d.ts.map +1 -0
- package/dist/__tests__/Sprint56.test.d.ts.map +1 -0
- package/dist/__tests__/collections.test.d.ts.map +1 -0
- package/dist/__tests__/events.test.d.ts.map +1 -0
- package/dist/__tests__/materials.test.d.ts.map +1 -0
- package/dist/__tests__/math.test.d.ts.map +1 -0
- package/dist/__tests__/physics.test.d.ts.map +1 -0
- package/dist/__tests__/spatial.test.d.ts.map +1 -0
- package/dist/__tests__/string-decoupled.test.d.ts.map +1 -0
- package/dist/__tests__/string.test.d.ts.map +1 -0
- package/dist/__tests__/time.test.d.ts.map +1 -0
- package/dist/collections.d.ts.map +1 -1
- package/dist/events.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -9
- package/dist/materials.d.ts.map +1 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/physics.d.ts.map +1 -0
- package/dist/spatial.d.ts.map +1 -0
- package/dist/string.d.ts.map +1 -1
- package/dist/time.d.ts.map +1 -1
- package/dist/traits/ARTraits.d.ts.map +1 -0
- package/dist/traits/EconomicPrimitives.d.ts.map +1 -0
- package/dist/traits/EconomicTraits.d.ts.map +1 -0
- package/dist/traits/IoTTraits.d.ts.map +1 -0
- package/dist/traits/SimulationLabPrimitives.d.ts.map +1 -0
- package/dist/traits/SimulationLabTraits.d.ts.map +1 -0
- package/dist/traits/VRRTraits.d.ts.map +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +17 -6
- package/src/__tests__/EconomicPrimitives.test.ts +690 -0
- package/src/__tests__/EconomicTraits.test.ts +425 -0
- package/src/__tests__/SimulationLabPrimitives.test.ts +415 -0
- package/src/__tests__/Sprint33.test.ts +1301 -0
- package/src/__tests__/Sprint56.test.ts +1070 -0
- package/src/__tests__/collections.test.ts +182 -0
- package/src/__tests__/events.test.ts +135 -0
- package/src/__tests__/materials.test.ts +84 -0
- package/src/__tests__/math.test.ts +246 -0
- package/src/__tests__/physics.test.ts +83 -0
- package/src/__tests__/spatial.test.ts +333 -0
- package/src/__tests__/string-decoupled.test.ts +16 -0
- package/src/__tests__/string.test.ts +164 -0
- package/src/__tests__/time.test.ts +110 -0
- package/src/collections.ts +8 -2
- package/src/events.ts +88 -0
- package/src/index.ts +161 -11
- package/src/materials.ts +109 -0
- package/src/math.ts +30 -14
- package/src/physics.ts +141 -0
- package/src/spatial.ts +320 -0
- package/src/string.basic.test.ts +14 -0
- package/src/string.test.ts +335 -0
- package/src/string.ts +19 -2
- package/src/time.ts +9 -10
- package/src/traits/ARTraits.ts +103 -0
- package/src/traits/EconomicPrimitives.ts +755 -0
- package/src/traits/EconomicTraits.ts +552 -0
- package/src/traits/IoTTraits.ts +102 -0
- package/src/traits/SimulationLabPrimitives.ts +650 -0
- package/src/traits/SimulationLabTraits.ts +191 -0
- package/src/traits/VRRTraits.ts +326 -0
- package/src/types.ts +47 -12
- package/vitest.config.ts +10 -0
- package/dist/collections.d.ts +0 -177
- package/dist/collections.js +0 -720
- package/dist/collections.js.map +0 -1
- package/dist/index.d.ts +0 -89
- package/dist/index.js.map +0 -1
- package/dist/math.d.ts +0 -162
- package/dist/math.js +0 -539
- package/dist/math.js.map +0 -1
- package/dist/string.d.ts +0 -208
- package/dist/string.js +0 -443
- package/dist/string.js.map +0 -1
- package/dist/time.d.ts +0 -215
- package/dist/time.js +0 -530
- package/dist/time.js.map +0 -1
- package/dist/types.d.ts +0 -193
- package/dist/types.js +0 -198
- package/dist/types.js.map +0 -1
package/src/spatial.ts
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @holoscript/std — Spatial Module
|
|
3
|
+
*
|
|
4
|
+
* Provides spatial math utilities: Vec3, Quaternion, Transform, Ray, AABB, distance.
|
|
5
|
+
* Used by the HoloScript runtime for spatial computing operations.
|
|
6
|
+
*
|
|
7
|
+
* @version 0.2.0
|
|
8
|
+
* @module @holoscript/std/spatial
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Vec3
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export class Vec3 {
|
|
16
|
+
constructor(
|
|
17
|
+
public x: number = 0,
|
|
18
|
+
public y: number = 0,
|
|
19
|
+
public z: number = 0
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
static zero(): Vec3 {
|
|
23
|
+
return new Vec3(0, 0, 0);
|
|
24
|
+
}
|
|
25
|
+
static one(): Vec3 {
|
|
26
|
+
return new Vec3(1, 1, 1);
|
|
27
|
+
}
|
|
28
|
+
static up(): Vec3 {
|
|
29
|
+
return new Vec3(0, 1, 0);
|
|
30
|
+
}
|
|
31
|
+
static down(): Vec3 {
|
|
32
|
+
return new Vec3(0, -1, 0);
|
|
33
|
+
}
|
|
34
|
+
static forward(): Vec3 {
|
|
35
|
+
return new Vec3(0, 0, -1);
|
|
36
|
+
}
|
|
37
|
+
static right(): Vec3 {
|
|
38
|
+
return new Vec3(1, 0, 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
add(other: Vec3): Vec3 {
|
|
42
|
+
return new Vec3(this.x + other.x, this.y + other.y, this.z + other.z);
|
|
43
|
+
}
|
|
44
|
+
sub(other: Vec3): Vec3 {
|
|
45
|
+
return new Vec3(this.x - other.x, this.y - other.y, this.z - other.z);
|
|
46
|
+
}
|
|
47
|
+
mul(s: number): Vec3 {
|
|
48
|
+
return new Vec3(this.x * s, this.y * s, this.z * s);
|
|
49
|
+
}
|
|
50
|
+
div(s: number): Vec3 {
|
|
51
|
+
return new Vec3(this.x / s, this.y / s, this.z / s);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
dot(other: Vec3): number {
|
|
55
|
+
return this.x * other.x + this.y * other.y + this.z * other.z;
|
|
56
|
+
}
|
|
57
|
+
cross(other: Vec3): Vec3 {
|
|
58
|
+
return new Vec3(
|
|
59
|
+
this.y * other.z - this.z * other.y,
|
|
60
|
+
this.z * other.x - this.x * other.z,
|
|
61
|
+
this.x * other.y - this.y * other.x
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
length(): number {
|
|
66
|
+
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
|
|
67
|
+
}
|
|
68
|
+
lengthSquared(): number {
|
|
69
|
+
return this.x * this.x + this.y * this.y + this.z * this.z;
|
|
70
|
+
}
|
|
71
|
+
normalize(): Vec3 {
|
|
72
|
+
const l = this.length();
|
|
73
|
+
return l > 0 ? this.div(l) : Vec3.zero();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
distanceTo(other: Vec3): number {
|
|
77
|
+
return this.sub(other).length();
|
|
78
|
+
}
|
|
79
|
+
lerp(other: Vec3, t: number): Vec3 {
|
|
80
|
+
return this.add(other.sub(this).mul(t));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
toArray(): [number, number, number] {
|
|
84
|
+
return [this.x, this.y, this.z];
|
|
85
|
+
}
|
|
86
|
+
static fromArray(arr: number[]): Vec3 {
|
|
87
|
+
return new Vec3(arr[0] || 0, arr[1] || 0, arr[2] || 0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
equals(other: Vec3, epsilon = 1e-6): boolean {
|
|
91
|
+
return (
|
|
92
|
+
Math.abs(this.x - other.x) < epsilon &&
|
|
93
|
+
Math.abs(this.y - other.y) < epsilon &&
|
|
94
|
+
Math.abs(this.z - other.z) < epsilon
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
toString(): string {
|
|
99
|
+
return `Vec3(${this.x}, ${this.y}, ${this.z})`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// =============================================================================
|
|
104
|
+
// Quaternion
|
|
105
|
+
// =============================================================================
|
|
106
|
+
|
|
107
|
+
export class Quaternion {
|
|
108
|
+
constructor(
|
|
109
|
+
public x = 0,
|
|
110
|
+
public y = 0,
|
|
111
|
+
public z = 0,
|
|
112
|
+
public w = 1
|
|
113
|
+
) {}
|
|
114
|
+
|
|
115
|
+
static identity(): Quaternion {
|
|
116
|
+
return new Quaternion(0, 0, 0, 1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static fromEuler(x: number, y: number, z: number): Quaternion {
|
|
120
|
+
const cx = Math.cos(x * 0.5),
|
|
121
|
+
sx = Math.sin(x * 0.5);
|
|
122
|
+
const cy = Math.cos(y * 0.5),
|
|
123
|
+
sy = Math.sin(y * 0.5);
|
|
124
|
+
const cz = Math.cos(z * 0.5),
|
|
125
|
+
sz = Math.sin(z * 0.5);
|
|
126
|
+
return new Quaternion(
|
|
127
|
+
sx * cy * cz - cx * sy * sz,
|
|
128
|
+
cx * sy * cz + sx * cy * sz,
|
|
129
|
+
cx * cy * sz - sx * sy * cz,
|
|
130
|
+
cx * cy * cz + sx * sy * sz
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static fromAxisAngle(axis: Vec3, angle: number): Quaternion {
|
|
135
|
+
const half = angle * 0.5;
|
|
136
|
+
const s = Math.sin(half);
|
|
137
|
+
const n = axis.normalize();
|
|
138
|
+
return new Quaternion(n.x * s, n.y * s, n.z * s, Math.cos(half));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
multiply(other: Quaternion): Quaternion {
|
|
142
|
+
return new Quaternion(
|
|
143
|
+
this.w * other.x + this.x * other.w + this.y * other.z - this.z * other.y,
|
|
144
|
+
this.w * other.y - this.x * other.z + this.y * other.w + this.z * other.x,
|
|
145
|
+
this.w * other.z + this.x * other.y - this.y * other.x + this.z * other.w,
|
|
146
|
+
this.w * other.w - this.x * other.x - this.y * other.y - this.z * other.z
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
rotateVec3(v: Vec3): Vec3 {
|
|
151
|
+
const u = new Vec3(this.x, this.y, this.z);
|
|
152
|
+
const s = this.w;
|
|
153
|
+
return u
|
|
154
|
+
.mul(2 * u.dot(v))
|
|
155
|
+
.add(v.mul(s * s - u.dot(u)))
|
|
156
|
+
.add(u.cross(v).mul(2 * s));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
slerp(other: Quaternion, t: number): Quaternion {
|
|
160
|
+
let dot = this.x * other.x + this.y * other.y + this.z * other.z + this.w * other.w;
|
|
161
|
+
const neg = dot < 0;
|
|
162
|
+
if (neg) dot = -dot;
|
|
163
|
+
if (dot > 0.9995) {
|
|
164
|
+
// Linear interpolation for very close quaternions
|
|
165
|
+
const r = new Quaternion(
|
|
166
|
+
this.x + t * ((neg ? -other.x : other.x) - this.x),
|
|
167
|
+
this.y + t * ((neg ? -other.y : other.y) - this.y),
|
|
168
|
+
this.z + t * ((neg ? -other.z : other.z) - this.z),
|
|
169
|
+
this.w + t * ((neg ? -other.w : other.w) - this.w)
|
|
170
|
+
);
|
|
171
|
+
const len = Math.sqrt(r.x * r.x + r.y * r.y + r.z * r.z + r.w * r.w);
|
|
172
|
+
return new Quaternion(r.x / len, r.y / len, r.z / len, r.w / len);
|
|
173
|
+
}
|
|
174
|
+
const theta = Math.acos(dot);
|
|
175
|
+
const sinTheta = Math.sin(theta);
|
|
176
|
+
const a = Math.sin((1 - t) * theta) / sinTheta;
|
|
177
|
+
const b = (Math.sin(t * theta) / sinTheta) * (neg ? -1 : 1);
|
|
178
|
+
return new Quaternion(
|
|
179
|
+
a * this.x + b * other.x,
|
|
180
|
+
a * this.y + b * other.y,
|
|
181
|
+
a * this.z + b * other.z,
|
|
182
|
+
a * this.w + b * other.w
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
normalize(): Quaternion {
|
|
187
|
+
const len = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w);
|
|
188
|
+
return new Quaternion(this.x / len, this.y / len, this.z / len, this.w / len);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
toEuler(): Vec3 {
|
|
192
|
+
const sinr = 2 * (this.w * this.x + this.y * this.z);
|
|
193
|
+
const cosr = 1 - 2 * (this.x * this.x + this.y * this.y);
|
|
194
|
+
const sinp = 2 * (this.w * this.y - this.z * this.x);
|
|
195
|
+
const siny = 2 * (this.w * this.z + this.x * this.y);
|
|
196
|
+
const cosy = 1 - 2 * (this.y * this.y + this.z * this.z);
|
|
197
|
+
return new Vec3(
|
|
198
|
+
Math.atan2(sinr, cosr),
|
|
199
|
+
Math.abs(sinp) >= 1 ? (Math.sign(sinp) * Math.PI) / 2 : Math.asin(sinp),
|
|
200
|
+
Math.atan2(siny, cosy)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// =============================================================================
|
|
206
|
+
// Transform
|
|
207
|
+
// =============================================================================
|
|
208
|
+
|
|
209
|
+
export class Transform {
|
|
210
|
+
constructor(
|
|
211
|
+
public position: Vec3 = Vec3.zero(),
|
|
212
|
+
public rotation: Quaternion = Quaternion.identity(),
|
|
213
|
+
public scale: Vec3 = Vec3.one()
|
|
214
|
+
) {}
|
|
215
|
+
|
|
216
|
+
static identity(): Transform {
|
|
217
|
+
return new Transform();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
transformPoint(point: Vec3): Vec3 {
|
|
221
|
+
const scaled = new Vec3(point.x * this.scale.x, point.y * this.scale.y, point.z * this.scale.z);
|
|
222
|
+
return this.rotation.rotateVec3(scaled).add(this.position);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
transformDirection(dir: Vec3): Vec3 {
|
|
226
|
+
return this.rotation.rotateVec3(dir);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// =============================================================================
|
|
231
|
+
// Ray & AABB
|
|
232
|
+
// =============================================================================
|
|
233
|
+
|
|
234
|
+
export class Ray {
|
|
235
|
+
constructor(
|
|
236
|
+
public origin: Vec3,
|
|
237
|
+
public direction: Vec3
|
|
238
|
+
) {}
|
|
239
|
+
|
|
240
|
+
pointAt(t: number): Vec3 {
|
|
241
|
+
return this.origin.add(this.direction.mul(t));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export class AABB {
|
|
246
|
+
constructor(
|
|
247
|
+
public min: Vec3,
|
|
248
|
+
public max: Vec3
|
|
249
|
+
) {}
|
|
250
|
+
|
|
251
|
+
contains(point: Vec3): boolean {
|
|
252
|
+
return (
|
|
253
|
+
point.x >= this.min.x &&
|
|
254
|
+
point.x <= this.max.x &&
|
|
255
|
+
point.y >= this.min.y &&
|
|
256
|
+
point.y <= this.max.y &&
|
|
257
|
+
point.z >= this.min.z &&
|
|
258
|
+
point.z <= this.max.z
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
intersects(other: AABB): boolean {
|
|
263
|
+
return (
|
|
264
|
+
this.min.x <= other.max.x &&
|
|
265
|
+
this.max.x >= other.min.x &&
|
|
266
|
+
this.min.y <= other.max.y &&
|
|
267
|
+
this.max.y >= other.min.y &&
|
|
268
|
+
this.min.z <= other.max.z &&
|
|
269
|
+
this.max.z >= other.min.z
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
center(): Vec3 {
|
|
274
|
+
return this.min.add(this.max).div(2);
|
|
275
|
+
}
|
|
276
|
+
size(): Vec3 {
|
|
277
|
+
return this.max.sub(this.min);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
intersectsRay(ray: Ray): number | null {
|
|
281
|
+
let tmin = (this.min.x - ray.origin.x) / ray.direction.x;
|
|
282
|
+
let tmax = (this.max.x - ray.origin.x) / ray.direction.x;
|
|
283
|
+
if (tmin > tmax) [tmin, tmax] = [tmax, tmin];
|
|
284
|
+
|
|
285
|
+
let tymin = (this.min.y - ray.origin.y) / ray.direction.y;
|
|
286
|
+
let tymax = (this.max.y - ray.origin.y) / ray.direction.y;
|
|
287
|
+
if (tymin > tymax) [tymin, tymax] = [tymax, tymin];
|
|
288
|
+
|
|
289
|
+
if (tmin > tymax || tymin > tmax) return null;
|
|
290
|
+
tmin = Math.max(tmin, tymin);
|
|
291
|
+
tmax = Math.min(tmax, tymax);
|
|
292
|
+
|
|
293
|
+
let tzmin = (this.min.z - ray.origin.z) / ray.direction.z;
|
|
294
|
+
let tzmax = (this.max.z - ray.origin.z) / ray.direction.z;
|
|
295
|
+
if (tzmin > tzmax) [tzmin, tzmax] = [tzmax, tzmin];
|
|
296
|
+
|
|
297
|
+
if (tmin > tzmax || tzmin > tmax) return null;
|
|
298
|
+
return Math.max(tmin, tzmin);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// =============================================================================
|
|
303
|
+
// Utility Functions
|
|
304
|
+
// =============================================================================
|
|
305
|
+
|
|
306
|
+
export function distance(a: Vec3, b: Vec3): number {
|
|
307
|
+
return a.distanceTo(b);
|
|
308
|
+
}
|
|
309
|
+
export function lerp(a: number, b: number, t: number): number {
|
|
310
|
+
return a + (b - a) * t;
|
|
311
|
+
}
|
|
312
|
+
export function clamp(v: number, min: number, max: number): number {
|
|
313
|
+
return Math.min(Math.max(v, min), max);
|
|
314
|
+
}
|
|
315
|
+
export function degToRad(deg: number): number {
|
|
316
|
+
return deg * (Math.PI / 180);
|
|
317
|
+
}
|
|
318
|
+
export function radToDeg(rad: number): number {
|
|
319
|
+
return rad * (180 / Math.PI);
|
|
320
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Basic test to verify formatBytes function
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { formatBytes } from './string.js';
|
|
7
|
+
|
|
8
|
+
describe('formatBytes basic test', () => {
|
|
9
|
+
it('should format bytes correctly', () => {
|
|
10
|
+
expect(formatBytes(0)).toBe('0 B');
|
|
11
|
+
expect(formatBytes(1024)).toBe('1 KB');
|
|
12
|
+
expect(formatBytes(1048576)).toBe('1 MB');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Comprehensive tests for @holoscript/std string utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
isBlank,
|
|
8
|
+
isNotBlank,
|
|
9
|
+
capitalize,
|
|
10
|
+
titleCase,
|
|
11
|
+
camelCase,
|
|
12
|
+
pascalCase,
|
|
13
|
+
snakeCase,
|
|
14
|
+
kebabCase,
|
|
15
|
+
constantCase,
|
|
16
|
+
padLeft,
|
|
17
|
+
padRight,
|
|
18
|
+
center,
|
|
19
|
+
truncate,
|
|
20
|
+
truncateMiddle,
|
|
21
|
+
repeat,
|
|
22
|
+
reverse,
|
|
23
|
+
count,
|
|
24
|
+
containsIgnoreCase,
|
|
25
|
+
startsWithIgnoreCase,
|
|
26
|
+
endsWithIgnoreCase,
|
|
27
|
+
removeWhitespace,
|
|
28
|
+
collapseWhitespace,
|
|
29
|
+
removePrefix,
|
|
30
|
+
removeSuffix,
|
|
31
|
+
wrap,
|
|
32
|
+
unwrap,
|
|
33
|
+
lines,
|
|
34
|
+
words,
|
|
35
|
+
chars,
|
|
36
|
+
join,
|
|
37
|
+
format,
|
|
38
|
+
formatNumber,
|
|
39
|
+
numberWithCommas,
|
|
40
|
+
formatBytes,
|
|
41
|
+
formatDuration,
|
|
42
|
+
escapeHtml,
|
|
43
|
+
unescapeHtml,
|
|
44
|
+
escapeRegex,
|
|
45
|
+
slugify,
|
|
46
|
+
isValidIdentifier,
|
|
47
|
+
isNumeric,
|
|
48
|
+
isAlphanumeric,
|
|
49
|
+
isAlpha,
|
|
50
|
+
randomString,
|
|
51
|
+
uuid,
|
|
52
|
+
indent,
|
|
53
|
+
dedent,
|
|
54
|
+
wordWrap,
|
|
55
|
+
levenshtein,
|
|
56
|
+
similarity,
|
|
57
|
+
extractTraits
|
|
58
|
+
} from './string.js';
|
|
59
|
+
|
|
60
|
+
describe('@holoscript/std string utilities', () => {
|
|
61
|
+
describe('formatBytes', () => {
|
|
62
|
+
it('should format bytes correctly with default 2 decimals', () => {
|
|
63
|
+
expect(formatBytes(0)).toBe('0 B');
|
|
64
|
+
expect(formatBytes(100)).toBe('100 B');
|
|
65
|
+
expect(formatBytes(1023)).toBe('1023 B');
|
|
66
|
+
expect(formatBytes(1024)).toBe('1 KB');
|
|
67
|
+
expect(formatBytes(1536)).toBe('1.5 KB');
|
|
68
|
+
expect(formatBytes(1048576)).toBe('1 MB');
|
|
69
|
+
expect(formatBytes(1073741824)).toBe('1 GB');
|
|
70
|
+
expect(formatBytes(1099511627776)).toBe('1 TB');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should respect custom decimal precision', () => {
|
|
74
|
+
expect(formatBytes(1536, 0)).toBe('2 KB');
|
|
75
|
+
expect(formatBytes(1536, 1)).toBe('1.5 KB');
|
|
76
|
+
expect(formatBytes(1536, 3)).toBe('1.5 KB');
|
|
77
|
+
expect(formatBytes(1677721, 3)).toBe('1.6 MB');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle edge cases', () => {
|
|
81
|
+
expect(formatBytes(1)).toBe('1 B');
|
|
82
|
+
expect(formatBytes(1025)).toBe('1 KB');
|
|
83
|
+
expect(formatBytes(-100)).toBe('-100 B');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should handle very large numbers', () => {
|
|
87
|
+
expect(formatBytes(1125899906842624)).toBe('1 PB'); // 1 petabyte
|
|
88
|
+
expect(formatBytes(2251799813685248)).toBe('2 PB');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('formatDuration', () => {
|
|
93
|
+
it('should format milliseconds correctly', () => {
|
|
94
|
+
expect(formatDuration(0)).toBe('0ms');
|
|
95
|
+
expect(formatDuration(100)).toBe('100ms');
|
|
96
|
+
expect(formatDuration(999)).toBe('999ms');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should format seconds correctly', () => {
|
|
100
|
+
expect(formatDuration(1000)).toBe('1.0s');
|
|
101
|
+
expect(formatDuration(1500)).toBe('1.5s');
|
|
102
|
+
expect(formatDuration(59999)).toBe('60.0s');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should format minutes correctly', () => {
|
|
106
|
+
expect(formatDuration(60000)).toBe('1.0m');
|
|
107
|
+
expect(formatDuration(90000)).toBe('1.5m');
|
|
108
|
+
expect(formatDuration(3599999)).toBe('60.0m');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should format hours correctly', () => {
|
|
112
|
+
expect(formatDuration(3600000)).toBe('1.0h');
|
|
113
|
+
expect(formatDuration(5400000)).toBe('1.5h');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('case conversion', () => {
|
|
118
|
+
const testString = 'hello world test';
|
|
119
|
+
|
|
120
|
+
it('capitalize should capitalize first letter', () => {
|
|
121
|
+
expect(capitalize('')).toBe('');
|
|
122
|
+
expect(capitalize('hello')).toBe('Hello');
|
|
123
|
+
expect(capitalize(testString)).toBe('Hello world test');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('titleCase should capitalize each word', () => {
|
|
127
|
+
expect(titleCase(testString)).toBe('Hello World Test');
|
|
128
|
+
expect(titleCase('')).toBe('');
|
|
129
|
+
expect(titleCase('a')).toBe('A');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('camelCase should convert to camelCase', () => {
|
|
133
|
+
expect(camelCase(testString)).toBe('helloWorldTest');
|
|
134
|
+
expect(camelCase('Hello World')).toBe('helloWorld');
|
|
135
|
+
expect(camelCase('')).toBe('');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('pascalCase should convert to PascalCase', () => {
|
|
139
|
+
expect(pascalCase(testString)).toBe('HelloWorldTest');
|
|
140
|
+
expect(pascalCase('hello world')).toBe('HelloWorld');
|
|
141
|
+
expect(pascalCase('')).toBe('');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('snakeCase should convert to snake_case', () => {
|
|
145
|
+
expect(snakeCase(testString)).toBe('hello_world_test');
|
|
146
|
+
expect(snakeCase('HelloWorld')).toBe('hello_world');
|
|
147
|
+
expect(snakeCase('')).toBe('');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('kebabCase should convert to kebab-case', () => {
|
|
151
|
+
expect(kebabCase(testString)).toBe('hello-world-test');
|
|
152
|
+
expect(kebabCase('HelloWorld')).toBe('hello-world');
|
|
153
|
+
expect(kebabCase('')).toBe('');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('constantCase should convert to CONSTANT_CASE', () => {
|
|
157
|
+
expect(constantCase(testString)).toBe('HELLO_WORLD_TEST');
|
|
158
|
+
expect(constantCase('HelloWorld')).toBe('HELLO_WORLD');
|
|
159
|
+
expect(constantCase('')).toBe('');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('padding and alignment', () => {
|
|
164
|
+
it('padLeft should pad on the left', () => {
|
|
165
|
+
expect(padLeft('test', 8)).toBe(' test');
|
|
166
|
+
expect(padLeft('test', 8, '*')).toBe('****test');
|
|
167
|
+
expect(padLeft('test', 2)).toBe('test'); // No padding if already long enough
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('padRight should pad on the right', () => {
|
|
171
|
+
expect(padRight('test', 8)).toBe('test ');
|
|
172
|
+
expect(padRight('test', 8, '*')).toBe('test****');
|
|
173
|
+
expect(padRight('test', 2)).toBe('test');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('center should center text', () => {
|
|
177
|
+
expect(center('test', 8)).toBe(' test ');
|
|
178
|
+
expect(center('test', 9)).toBe(' test ');
|
|
179
|
+
expect(center('test', 8, '*')).toBe('**test**');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('validation functions', () => {
|
|
184
|
+
describe('isBlank/isNotBlank', () => {
|
|
185
|
+
it('should detect blank strings', () => {
|
|
186
|
+
expect(isBlank('')).toBe(true);
|
|
187
|
+
expect(isBlank(' ')).toBe(true);
|
|
188
|
+
expect(isBlank('\t\n')).toBe(true);
|
|
189
|
+
expect(isBlank('test')).toBe(false);
|
|
190
|
+
|
|
191
|
+
expect(isNotBlank('')).toBe(false);
|
|
192
|
+
expect(isNotBlank(' ')).toBe(false);
|
|
193
|
+
expect(isNotBlank('test')).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('isValidIdentifier should validate identifiers', () => {
|
|
198
|
+
expect(isValidIdentifier('validName')).toBe(true);
|
|
199
|
+
expect(isValidIdentifier('_validName')).toBe(true);
|
|
200
|
+
expect(isValidIdentifier('$validName')).toBe(true);
|
|
201
|
+
expect(isValidIdentifier('123invalid')).toBe(false);
|
|
202
|
+
expect(isValidIdentifier('invalid-name')).toBe(false);
|
|
203
|
+
expect(isValidIdentifier('')).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('isNumeric should validate numeric strings', () => {
|
|
207
|
+
expect(isNumeric('123')).toBe(true);
|
|
208
|
+
expect(isNumeric('123.45')).toBe(true);
|
|
209
|
+
expect(isNumeric('-123')).toBe(true);
|
|
210
|
+
expect(isNumeric('abc')).toBe(false);
|
|
211
|
+
expect(isNumeric('')).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('isAlphanumeric should validate alphanumeric strings', () => {
|
|
215
|
+
expect(isAlphanumeric('abc123')).toBe(true);
|
|
216
|
+
expect(isAlphanumeric('ABC')).toBe(true);
|
|
217
|
+
expect(isAlphanumeric('123')).toBe(true);
|
|
218
|
+
expect(isAlphanumeric('abc-123')).toBe(false);
|
|
219
|
+
expect(isAlphanumeric('')).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('isAlpha should validate alphabetic strings', () => {
|
|
223
|
+
expect(isAlpha('abc')).toBe(true);
|
|
224
|
+
expect(isAlpha('ABC')).toBe(true);
|
|
225
|
+
expect(isAlpha('abc123')).toBe(false);
|
|
226
|
+
expect(isAlpha('')).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('string manipulation', () => {
|
|
231
|
+
it('truncate should truncate with ellipsis', () => {
|
|
232
|
+
expect(truncate('hello world', 5)).toBe('he...');
|
|
233
|
+
expect(truncate('hello', 10)).toBe('hello');
|
|
234
|
+
expect(truncate('hello world', 8, '---')).toBe('hello---');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('truncateMiddle should truncate in middle', () => {
|
|
238
|
+
expect(truncateMiddle('hello world test', 10)).toBe('hel...test');
|
|
239
|
+
expect(truncateMiddle('short', 10)).toBe('short');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('repeat should repeat strings', () => {
|
|
243
|
+
expect(repeat('abc', 3)).toBe('abcabcabc');
|
|
244
|
+
expect(repeat('x', 0)).toBe('');
|
|
245
|
+
expect(repeat('', 5)).toBe('');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('reverse should reverse strings', () => {
|
|
249
|
+
expect(reverse('hello')).toBe('olleh');
|
|
250
|
+
expect(reverse('')).toBe('');
|
|
251
|
+
expect(reverse('a')).toBe('a');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('HTML utilities', () => {
|
|
256
|
+
it('escapeHtml should escape HTML entities', () => {
|
|
257
|
+
expect(escapeHtml('<div>test</div>')).toBe('<div>test</div>');
|
|
258
|
+
expect(escapeHtml('a & b')).toBe('a & b');
|
|
259
|
+
expect(escapeHtml('"quoted"')).toBe('"quoted"');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('unescapeHtml should unescape HTML entities', () => {
|
|
263
|
+
expect(unescapeHtml('<div>test</div>')).toBe('<div>test</div>');
|
|
264
|
+
expect(unescapeHtml('a & b')).toBe('a & b');
|
|
265
|
+
expect(unescapeHtml('"quoted"')).toBe('"quoted"');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('utility functions', () => {
|
|
270
|
+
it('count should count substring occurrences', () => {
|
|
271
|
+
expect(count('hello world hello', 'hello')).toBe(2);
|
|
272
|
+
expect(count('aaa', 'aa')).toBe(2); // Overlapping matches
|
|
273
|
+
expect(count('test', 'xyz')).toBe(0);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('uuid should generate valid UUIDs', () => {
|
|
277
|
+
const id1 = uuid();
|
|
278
|
+
const id2 = uuid();
|
|
279
|
+
expect(id1).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
280
|
+
expect(id2).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
281
|
+
expect(id1).not.toBe(id2); // Should be unique
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('randomString should generate random strings', () => {
|
|
285
|
+
const str1 = randomString(10);
|
|
286
|
+
const str2 = randomString(10);
|
|
287
|
+
expect(str1.length).toBe(10);
|
|
288
|
+
expect(str2.length).toBe(10);
|
|
289
|
+
expect(str1).not.toBe(str2); // Should be different
|
|
290
|
+
|
|
291
|
+
const customStr = randomString(5, 'ABC');
|
|
292
|
+
expect(customStr.length).toBe(5);
|
|
293
|
+
expect(customStr).toMatch(/^[ABC]+$/);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('levenshtein should calculate edit distance', () => {
|
|
297
|
+
expect(levenshtein('', '')).toBe(0);
|
|
298
|
+
expect(levenshtein('hello', 'hello')).toBe(0);
|
|
299
|
+
expect(levenshtein('hello', 'helo')).toBe(1);
|
|
300
|
+
expect(levenshtein('kitten', 'sitting')).toBe(3);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('similarity should calculate similarity score', () => {
|
|
304
|
+
expect(similarity('hello', 'hello')).toBe(1);
|
|
305
|
+
expect(similarity('', '')).toBe(1);
|
|
306
|
+
expect(similarity('hello', 'world')).toBeLessThan(0.5);
|
|
307
|
+
expect(similarity('hello', 'helo')).toBeGreaterThan(0.8);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('format utilities', () => {
|
|
312
|
+
it('numberWithCommas should add commas to numbers', () => {
|
|
313
|
+
expect(numberWithCommas(1000)).toBe('1,000');
|
|
314
|
+
expect(numberWithCommas(1234567)).toBe('1,234,567');
|
|
315
|
+
expect(numberWithCommas(100)).toBe('100');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('format should template strings', () => {
|
|
319
|
+
expect(format('Hello {name}!', { name: 'World' })).toBe('Hello World!');
|
|
320
|
+
expect(format('{a} + {b} = {sum}', { a: 1, b: 2, sum: 3 })).toBe('1 + 2 = 3');
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('extractTraits', () => {
|
|
325
|
+
it('should extract trait syntax from code', () => {
|
|
326
|
+
const code = `
|
|
327
|
+
<Entity position="[1,2,3]" rotation="[0,0,0,1]">
|
|
328
|
+
<Cube color="#ff0000" />
|
|
329
|
+
</Entity>
|
|
330
|
+
`;
|
|
331
|
+
const traits = extractTraits(code);
|
|
332
|
+
expect(traits).toEqual(['Entity', 'Cube']);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
package/src/string.ts
CHANGED
|
@@ -329,7 +329,10 @@ export function unescapeHtml(s: string): string {
|
|
|
329
329
|
''': "'",
|
|
330
330
|
'/': '/',
|
|
331
331
|
};
|
|
332
|
-
return s.replace(
|
|
332
|
+
return s.replace(
|
|
333
|
+
/&(?:amp|lt|gt|quot|#39|#x27|#x2F);/g,
|
|
334
|
+
(entity) => htmlEntities[entity] || entity
|
|
335
|
+
);
|
|
333
336
|
}
|
|
334
337
|
|
|
335
338
|
/**
|
|
@@ -382,7 +385,10 @@ export function isAlpha(s: string): boolean {
|
|
|
382
385
|
/**
|
|
383
386
|
* Generate a random string
|
|
384
387
|
*/
|
|
385
|
-
export function randomString(
|
|
388
|
+
export function randomString(
|
|
389
|
+
length: number,
|
|
390
|
+
charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
391
|
+
): string {
|
|
386
392
|
let result = '';
|
|
387
393
|
for (let i = 0; i < length; i++) {
|
|
388
394
|
result += charset[Math.floor(Math.random() * charset.length)];
|
|
@@ -497,3 +503,14 @@ export function similarity(a: string, b: string): number {
|
|
|
497
503
|
if (maxLen === 0) return 1;
|
|
498
504
|
return 1 - levenshtein(a, b) / maxLen;
|
|
499
505
|
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Extract HoloScript traits (e.g., @grabbable) from a code string
|
|
509
|
+
*/
|
|
510
|
+
export function extractTraits(code: string): string[] {
|
|
511
|
+
const re = /@([a-zA-Z_]\w*)/g;
|
|
512
|
+
const traits = new Set<string>();
|
|
513
|
+
let m;
|
|
514
|
+
while ((m = re.exec(code)) !== null) traits.add(`@${m[1]}`);
|
|
515
|
+
return [...traits];
|
|
516
|
+
}
|