@holoscript/std 2.1.0 → 6.0.3
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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/__tests__/EconomicPrimitives.test.d.ts +2 -0
- package/dist/__tests__/EconomicPrimitives.test.d.ts.map +1 -0
- package/dist/__tests__/EconomicTraits.test.d.ts +2 -0
- package/dist/__tests__/EconomicTraits.test.d.ts.map +1 -0
- package/dist/__tests__/SimulationLabPrimitives.test.d.ts +2 -0
- package/dist/__tests__/SimulationLabPrimitives.test.d.ts.map +1 -0
- package/dist/__tests__/collections.test.d.ts +2 -0
- package/dist/__tests__/collections.test.d.ts.map +1 -0
- package/dist/__tests__/events.test.d.ts +2 -0
- package/dist/__tests__/events.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/materials.test.d.ts +2 -0
- package/dist/__tests__/materials.test.d.ts.map +1 -0
- package/dist/__tests__/math.test.d.ts +2 -0
- package/dist/__tests__/math.test.d.ts.map +1 -0
- package/dist/__tests__/physics.test.d.ts +2 -0
- package/dist/__tests__/physics.test.d.ts.map +1 -0
- package/dist/__tests__/spatial.test.d.ts +2 -0
- package/dist/__tests__/spatial.test.d.ts.map +1 -0
- package/dist/__tests__/standard-library-fundamentals.test.d.ts +2 -0
- package/dist/__tests__/standard-library-fundamentals.test.d.ts.map +1 -0
- package/dist/__tests__/string-and-time-utilities.test.d.ts +2 -0
- package/dist/__tests__/string-and-time-utilities.test.d.ts.map +1 -0
- package/dist/__tests__/string-decoupled.test.d.ts +2 -0
- package/dist/__tests__/string-decoupled.test.d.ts.map +1 -0
- package/dist/__tests__/string.test.d.ts +2 -0
- package/dist/__tests__/string.test.d.ts.map +1 -0
- package/dist/__tests__/time.test.d.ts +2 -0
- package/dist/__tests__/time.test.d.ts.map +1 -0
- package/dist/chunk-7HVUYGPS.js +679 -0
- package/dist/chunk-7HVUYGPS.js.map +1 -0
- package/dist/chunk-P4O3WNG2.js +287 -0
- package/dist/chunk-P4O3WNG2.js.map +1 -0
- package/dist/chunk-PFX2JM4X.js +683 -0
- package/dist/chunk-PFX2JM4X.js.map +1 -0
- package/dist/chunk-W2Q3LUCM.js +348 -0
- package/dist/chunk-W2Q3LUCM.js.map +1 -0
- package/dist/chunk-WS6W35YC.js +443 -0
- package/dist/chunk-WS6W35YC.js.map +1 -0
- package/dist/chunk-ZXVZAQCZ.js +470 -0
- package/dist/chunk-ZXVZAQCZ.js.map +1 -0
- package/dist/collections.cjs +715 -0
- package/dist/collections.cjs.map +1 -0
- package/dist/collections.d.ts.map +1 -1
- package/dist/collections.js +16 -719
- package/dist/collections.js.map +1 -1
- package/dist/events.d.ts +32 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/index.cjs +4101 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +21 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1270 -157
- package/dist/index.js.map +1 -1
- package/dist/materials.d.ts +50 -0
- package/dist/materials.d.ts.map +1 -0
- package/dist/math.cjs +591 -0
- package/dist/math.cjs.map +1 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +51 -537
- package/dist/math.js.map +1 -1
- package/dist/physics.d.ts +59 -0
- package/dist/physics.d.ts.map +1 -0
- package/dist/spatial.d.ts +82 -0
- package/dist/spatial.d.ts.map +1 -0
- package/dist/string.basic.test.d.ts +5 -0
- package/dist/string.basic.test.d.ts.map +1 -0
- package/dist/string.cjs +422 -0
- package/dist/string.cjs.map +1 -0
- package/dist/string.d.ts +5 -1
- package/dist/string.d.ts.map +1 -1
- package/dist/string.js +106 -442
- package/dist/string.js.map +1 -1
- package/dist/string.test.d.ts +5 -0
- package/dist/string.test.d.ts.map +1 -0
- package/dist/time.cjs +512 -0
- package/dist/time.cjs.map +1 -0
- package/dist/time.d.ts.map +1 -1
- package/dist/time.js +41 -528
- package/dist/time.js.map +1 -1
- package/dist/traits/ARTraits.d.ts +14 -0
- package/dist/traits/ARTraits.d.ts.map +1 -0
- package/dist/traits/EconomicPrimitives.cjs +335 -0
- package/dist/traits/EconomicPrimitives.cjs.map +1 -0
- package/dist/traits/EconomicPrimitives.d.ts +345 -0
- package/dist/traits/EconomicPrimitives.d.ts.map +1 -0
- package/dist/traits/EconomicPrimitives.js +55 -0
- package/dist/traits/EconomicPrimitives.js.map +1 -0
- package/dist/traits/EconomicTraits.cjs +471 -0
- package/dist/traits/EconomicTraits.cjs.map +1 -0
- package/dist/traits/EconomicTraits.d.ts +71 -0
- package/dist/traits/EconomicTraits.d.ts.map +1 -0
- package/dist/traits/EconomicTraits.js +17 -0
- package/dist/traits/EconomicTraits.js.map +1 -0
- package/dist/traits/IoTTraits.d.ts +14 -0
- package/dist/traits/IoTTraits.d.ts.map +1 -0
- package/dist/traits/SimulationLabPrimitives.d.ts +187 -0
- package/dist/traits/SimulationLabPrimitives.d.ts.map +1 -0
- package/dist/traits/SimulationLabTraits.d.ts +42 -0
- package/dist/traits/SimulationLabTraits.d.ts.map +1 -0
- package/dist/traits/VRRTraits.d.ts +150 -0
- package/dist/traits/VRRTraits.d.ts.map +1 -0
- package/dist/traits/__tests__/ARTraits.test.d.ts +2 -0
- package/dist/traits/__tests__/ARTraits.test.d.ts.map +1 -0
- package/dist/traits/__tests__/EconomicPrimitives.test.d.ts +2 -0
- package/dist/traits/__tests__/EconomicPrimitives.test.d.ts.map +1 -0
- package/dist/traits/__tests__/EconomicTraits.test.d.ts +2 -0
- package/dist/traits/__tests__/EconomicTraits.test.d.ts.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/error-utf8.log +6 -0
- package/error.log +0 -0
- package/package.json +42 -19
- 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__/collections.test.ts +182 -0
- package/src/__tests__/events.test.ts +135 -0
- package/src/__tests__/index.test.ts +14 -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__/standard-library-fundamentals.test.ts +1301 -0
- package/src/__tests__/string-and-time-utilities.test.ts +1070 -0
- package/src/__tests__/string-decoupled.test.ts +14 -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 +45 -13
- package/src/time.ts +10 -11
- 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/traits/__tests__/ARTraits.test.ts +14 -0
- package/src/traits/__tests__/EconomicPrimitives.test.ts +14 -0
- package/src/traits/__tests__/EconomicTraits.test.ts +14 -0
- package/src/types.ts +47 -12
- package/std_error.log +5 -0
- package/tsup.config.ts +17 -0
- package/vitest.config.ts +10 -0
- package/dist/types.js +0 -198
- package/dist/types.js.map +0 -1
|
@@ -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
|
@@ -116,8 +116,8 @@ export function truncate(s: string, maxLength: number, ellipsis = '...'): string
|
|
|
116
116
|
export function truncateMiddle(s: string, maxLength: number, ellipsis = '...'): string {
|
|
117
117
|
if (s.length <= maxLength) return s;
|
|
118
118
|
const availableLength = maxLength - ellipsis.length;
|
|
119
|
-
const leftLength = Math.
|
|
120
|
-
const rightLength = Math.
|
|
119
|
+
const leftLength = Math.floor(availableLength / 2);
|
|
120
|
+
const rightLength = Math.ceil(availableLength / 2);
|
|
121
121
|
return s.slice(0, leftLength) + ellipsis + s.slice(-rightLength);
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -144,7 +144,7 @@ export function count(s: string, substring: string): number {
|
|
|
144
144
|
let pos = 0;
|
|
145
145
|
while ((pos = s.indexOf(substring, pos)) !== -1) {
|
|
146
146
|
count++;
|
|
147
|
-
pos +=
|
|
147
|
+
pos += 1;
|
|
148
148
|
}
|
|
149
149
|
return count;
|
|
150
150
|
}
|
|
@@ -281,6 +281,7 @@ export function numberWithCommas(n: number): string {
|
|
|
281
281
|
export function formatBytes(bytes: number, decimals = 2): string {
|
|
282
282
|
if (bytes === 0) return '0 B';
|
|
283
283
|
const k = 1024;
|
|
284
|
+
if (bytes < 0) return `${bytes} B`;
|
|
284
285
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
285
286
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
286
287
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
|
@@ -293,13 +294,11 @@ export function formatDuration(ms: number): string {
|
|
|
293
294
|
if (ms < 1000) return `${ms}ms`;
|
|
294
295
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
295
296
|
if (ms < 3600000) {
|
|
296
|
-
|
|
297
|
-
const secs = Math.floor((ms % 60000) / 1000);
|
|
298
|
-
return `${mins}m ${secs}s`;
|
|
297
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
299
298
|
}
|
|
300
299
|
const hours = Math.floor(ms / 3600000);
|
|
301
300
|
const mins = Math.floor((ms % 3600000) / 60000);
|
|
302
|
-
return `${
|
|
301
|
+
return `${(ms / 3600000).toFixed(1)}h`;
|
|
303
302
|
}
|
|
304
303
|
|
|
305
304
|
/**
|
|
@@ -329,7 +328,10 @@ export function unescapeHtml(s: string): string {
|
|
|
329
328
|
''': "'",
|
|
330
329
|
'/': '/',
|
|
331
330
|
};
|
|
332
|
-
return s.replace(
|
|
331
|
+
return s.replace(
|
|
332
|
+
/&(?:amp|lt|gt|quot|#39|#x27|#x2F);/g,
|
|
333
|
+
(entity) => htmlEntities[entity] || entity
|
|
334
|
+
);
|
|
333
335
|
}
|
|
334
336
|
|
|
335
337
|
/**
|
|
@@ -382,7 +384,10 @@ export function isAlpha(s: string): boolean {
|
|
|
382
384
|
/**
|
|
383
385
|
* Generate a random string
|
|
384
386
|
*/
|
|
385
|
-
export function randomString(
|
|
387
|
+
export function randomString(
|
|
388
|
+
length: number,
|
|
389
|
+
charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
390
|
+
): string {
|
|
386
391
|
let result = '';
|
|
387
392
|
for (let i = 0; i < length; i++) {
|
|
388
393
|
result += charset[Math.floor(Math.random() * charset.length)];
|
|
@@ -489,11 +494,38 @@ export function levenshtein(a: string, b: string): number {
|
|
|
489
494
|
return matrix[b.length][a.length];
|
|
490
495
|
}
|
|
491
496
|
|
|
497
|
+
function lcsLength(a: string, b: string): number {
|
|
498
|
+
const n = b.length;
|
|
499
|
+
const dp: number[] = new Array(n + 1).fill(0);
|
|
500
|
+
for (let i = 0; i < a.length; i++) {
|
|
501
|
+
let prev = 0;
|
|
502
|
+
for (let j = 0; j < n; j++) {
|
|
503
|
+
const tmp = dp[j + 1];
|
|
504
|
+
dp[j + 1] = a[i] === b[j] ? prev + 1 : Math.max(dp[j + 1], dp[j]);
|
|
505
|
+
prev = tmp;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return dp[n];
|
|
509
|
+
}
|
|
510
|
+
|
|
492
511
|
/**
|
|
493
|
-
* Check similarity between two strings (0-1)
|
|
512
|
+
* Check similarity between two strings (0-1) using Sorensen-Dice over LCS
|
|
494
513
|
*/
|
|
495
514
|
export function similarity(a: string, b: string): number {
|
|
496
|
-
const
|
|
497
|
-
if (
|
|
498
|
-
return
|
|
515
|
+
const total = a.length + b.length;
|
|
516
|
+
if (total === 0) return 1;
|
|
517
|
+
return (2 * lcsLength(a, b)) / total;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Extract HoloScript trait annotations (@name) and JSX component names from code
|
|
522
|
+
*/
|
|
523
|
+
export function extractTraits(code: string): string[] {
|
|
524
|
+
const traits = new Set<string>();
|
|
525
|
+
let m: RegExpExecArray | null;
|
|
526
|
+
const atRe = /@([a-zA-Z_]\w*)/g;
|
|
527
|
+
while ((m = atRe.exec(code)) !== null) traits.add(`@${m[1]}`);
|
|
528
|
+
const tagRe = /<([A-Z][a-zA-Z0-9]*)\b/g;
|
|
529
|
+
while ((m = tagRe.exec(code)) !== null) traits.add(m[1]);
|
|
530
|
+
return [...traits];
|
|
499
531
|
}
|
package/src/time.ts
CHANGED
|
@@ -121,14 +121,11 @@ export function throttle<T extends (...args: unknown[]) => unknown>(fn: T, ms: n
|
|
|
121
121
|
lastCall = now();
|
|
122
122
|
fn(...args);
|
|
123
123
|
} else if (!timeoutId) {
|
|
124
|
-
timeoutId = setTimeout(
|
|
125
|
-
()
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
},
|
|
130
|
-
ms - elapsed
|
|
131
|
-
);
|
|
124
|
+
timeoutId = setTimeout(() => {
|
|
125
|
+
lastCall = now();
|
|
126
|
+
timeoutId = null;
|
|
127
|
+
fn(...args);
|
|
128
|
+
}, ms - elapsed);
|
|
132
129
|
}
|
|
133
130
|
}) as T;
|
|
134
131
|
}
|
|
@@ -136,7 +133,9 @@ export function throttle<T extends (...args: unknown[]) => unknown>(fn: T, ms: n
|
|
|
136
133
|
/**
|
|
137
134
|
* Measure execution time of a function
|
|
138
135
|
*/
|
|
139
|
-
export async function measure<T>(
|
|
136
|
+
export async function measure<T>(
|
|
137
|
+
fn: () => T | Promise<T>
|
|
138
|
+
): Promise<{ result: T; duration: number }> {
|
|
140
139
|
const start = hrTime();
|
|
141
140
|
const result = await fn();
|
|
142
141
|
const duration = hrTime() - start;
|
|
@@ -152,7 +151,7 @@ export async function timed<T>(label: string, fn: () => T | Promise<T>): Promise
|
|
|
152
151
|
return await fn();
|
|
153
152
|
} finally {
|
|
154
153
|
const duration = hrTime() - start;
|
|
155
|
-
|
|
154
|
+
void duration; // timing available for callers via return value
|
|
156
155
|
}
|
|
157
156
|
}
|
|
158
157
|
|
|
@@ -585,7 +584,7 @@ export const DateTime = {
|
|
|
585
584
|
* Add time to a date
|
|
586
585
|
*/
|
|
587
586
|
add(date: Date, amount: number, unit: 'ms' | 's' | 'm' | 'h' | 'd'): Date {
|
|
588
|
-
const
|
|
587
|
+
const _ms = DateTime.diff(new Date(amount), new Date(0), 'ms');
|
|
589
588
|
const multipliers = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
|
|
590
589
|
return new Date(date.getTime() + amount * multipliers[unit]);
|
|
591
590
|
},
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview AR Traits (Augmented Reality)
|
|
3
|
+
* @module @holoscript/std/traits
|
|
4
|
+
*
|
|
5
|
+
* Defines the AST traits required for augmented reality overlays
|
|
6
|
+
* and real-world physical anchoring.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { TraitDefinition } from '../types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Standard AR traits included in HoloScript's base functionality
|
|
13
|
+
*/
|
|
14
|
+
export const ARTraits: TraitDefinition[] = [
|
|
15
|
+
{
|
|
16
|
+
name: '@ar_beacon',
|
|
17
|
+
type: 'marker',
|
|
18
|
+
description:
|
|
19
|
+
'Binds a UI or 3D overlay to a physical reality marker (QR code, NFC, BLE beacon, or visual image).',
|
|
20
|
+
parameters: [
|
|
21
|
+
{
|
|
22
|
+
name: 'type',
|
|
23
|
+
type: 'string',
|
|
24
|
+
required: true,
|
|
25
|
+
validation: (val: any) => ['qr', 'image', 'nfc', 'ble', 'gps'].includes(val),
|
|
26
|
+
description: 'The type of physical beacon (qr, image, nfc, ble, gps)',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'id',
|
|
30
|
+
type: 'string',
|
|
31
|
+
required: true,
|
|
32
|
+
description: 'The unique identifier data the beacon broadcasts or visually represents',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'physical_size',
|
|
36
|
+
type: 'number',
|
|
37
|
+
required: false,
|
|
38
|
+
description:
|
|
39
|
+
'The expected physical size of the image/marker in meters (for image/qr types)',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
validation: (node: any) => {
|
|
43
|
+
return { valid: true }; // Allowed on any Node (Zones, Objects, UI)
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: '@overlay',
|
|
48
|
+
type: 'behavior',
|
|
49
|
+
description:
|
|
50
|
+
'Marks a HoloUI panel or 3D object to act as a pass-through overlay locked to the viewport or surface.',
|
|
51
|
+
parameters: [
|
|
52
|
+
{
|
|
53
|
+
name: 'anchor',
|
|
54
|
+
type: 'string',
|
|
55
|
+
required: false,
|
|
56
|
+
defaultValue: 'viewport',
|
|
57
|
+
validation: (val: any) => ['viewport', 'surface', 'plane', 'face', 'hand'].includes(val),
|
|
58
|
+
description: 'How the overlay anchors to the AR pass-through feed.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'occlusion',
|
|
62
|
+
type: 'boolean',
|
|
63
|
+
required: false,
|
|
64
|
+
defaultValue: true,
|
|
65
|
+
description: 'Whether physical real-world objects occlude this virtual overlay.',
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
validation: (node: any) => {
|
|
69
|
+
// Typically used on UI or SpatialGroups
|
|
70
|
+
if (node.type !== 'HoloUI' && node.type !== 'SpatialGroup') {
|
|
71
|
+
return { valid: false, message: '@overlay must be attached to HoloUI or SpatialGroup.' };
|
|
72
|
+
}
|
|
73
|
+
return { valid: true };
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: '@layer_shift',
|
|
78
|
+
type: 'behavior',
|
|
79
|
+
description:
|
|
80
|
+
'Controls the automatic transition between physical reality (AR) and digital twin reality (VR/VRR) boundaries.',
|
|
81
|
+
parameters: [
|
|
82
|
+
{
|
|
83
|
+
name: 'trigger',
|
|
84
|
+
type: 'string',
|
|
85
|
+
required: true,
|
|
86
|
+
validation: (val: any) => ['proximity', 'scan', 'payment', 'quest_step'].includes(val),
|
|
87
|
+
description: 'What triggers the reality shift.',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'to_layer',
|
|
91
|
+
type: 'string',
|
|
92
|
+
required: true,
|
|
93
|
+
validation: (val: any) => ['vr', 'vrr', 'ar'].includes(val),
|
|
94
|
+
description: 'Which layer the user shifts into.',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
validation: (node: any) => {
|
|
98
|
+
return { valid: true };
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
export default ARTraits;
|