@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.
Files changed (159) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +197 -0
  4. package/dist/__tests__/EconomicPrimitives.test.d.ts +2 -0
  5. package/dist/__tests__/EconomicPrimitives.test.d.ts.map +1 -0
  6. package/dist/__tests__/EconomicTraits.test.d.ts +2 -0
  7. package/dist/__tests__/EconomicTraits.test.d.ts.map +1 -0
  8. package/dist/__tests__/SimulationLabPrimitives.test.d.ts +2 -0
  9. package/dist/__tests__/SimulationLabPrimitives.test.d.ts.map +1 -0
  10. package/dist/__tests__/collections.test.d.ts +2 -0
  11. package/dist/__tests__/collections.test.d.ts.map +1 -0
  12. package/dist/__tests__/events.test.d.ts +2 -0
  13. package/dist/__tests__/events.test.d.ts.map +1 -0
  14. package/dist/__tests__/index.test.d.ts +2 -0
  15. package/dist/__tests__/index.test.d.ts.map +1 -0
  16. package/dist/__tests__/materials.test.d.ts +2 -0
  17. package/dist/__tests__/materials.test.d.ts.map +1 -0
  18. package/dist/__tests__/math.test.d.ts +2 -0
  19. package/dist/__tests__/math.test.d.ts.map +1 -0
  20. package/dist/__tests__/physics.test.d.ts +2 -0
  21. package/dist/__tests__/physics.test.d.ts.map +1 -0
  22. package/dist/__tests__/spatial.test.d.ts +2 -0
  23. package/dist/__tests__/spatial.test.d.ts.map +1 -0
  24. package/dist/__tests__/standard-library-fundamentals.test.d.ts +2 -0
  25. package/dist/__tests__/standard-library-fundamentals.test.d.ts.map +1 -0
  26. package/dist/__tests__/string-and-time-utilities.test.d.ts +2 -0
  27. package/dist/__tests__/string-and-time-utilities.test.d.ts.map +1 -0
  28. package/dist/__tests__/string-decoupled.test.d.ts +2 -0
  29. package/dist/__tests__/string-decoupled.test.d.ts.map +1 -0
  30. package/dist/__tests__/string.test.d.ts +2 -0
  31. package/dist/__tests__/string.test.d.ts.map +1 -0
  32. package/dist/__tests__/time.test.d.ts +2 -0
  33. package/dist/__tests__/time.test.d.ts.map +1 -0
  34. package/dist/chunk-7HVUYGPS.js +679 -0
  35. package/dist/chunk-7HVUYGPS.js.map +1 -0
  36. package/dist/chunk-P4O3WNG2.js +287 -0
  37. package/dist/chunk-P4O3WNG2.js.map +1 -0
  38. package/dist/chunk-PFX2JM4X.js +683 -0
  39. package/dist/chunk-PFX2JM4X.js.map +1 -0
  40. package/dist/chunk-W2Q3LUCM.js +348 -0
  41. package/dist/chunk-W2Q3LUCM.js.map +1 -0
  42. package/dist/chunk-WS6W35YC.js +443 -0
  43. package/dist/chunk-WS6W35YC.js.map +1 -0
  44. package/dist/chunk-ZXVZAQCZ.js +470 -0
  45. package/dist/chunk-ZXVZAQCZ.js.map +1 -0
  46. package/dist/collections.cjs +715 -0
  47. package/dist/collections.cjs.map +1 -0
  48. package/dist/collections.d.ts.map +1 -1
  49. package/dist/collections.js +16 -719
  50. package/dist/collections.js.map +1 -1
  51. package/dist/events.d.ts +32 -0
  52. package/dist/events.d.ts.map +1 -0
  53. package/dist/index.cjs +4101 -0
  54. package/dist/index.cjs.map +1 -0
  55. package/dist/index.d.ts +21 -9
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1270 -157
  58. package/dist/index.js.map +1 -1
  59. package/dist/materials.d.ts +50 -0
  60. package/dist/materials.d.ts.map +1 -0
  61. package/dist/math.cjs +591 -0
  62. package/dist/math.cjs.map +1 -0
  63. package/dist/math.d.ts.map +1 -1
  64. package/dist/math.js +51 -537
  65. package/dist/math.js.map +1 -1
  66. package/dist/physics.d.ts +59 -0
  67. package/dist/physics.d.ts.map +1 -0
  68. package/dist/spatial.d.ts +82 -0
  69. package/dist/spatial.d.ts.map +1 -0
  70. package/dist/string.basic.test.d.ts +5 -0
  71. package/dist/string.basic.test.d.ts.map +1 -0
  72. package/dist/string.cjs +422 -0
  73. package/dist/string.cjs.map +1 -0
  74. package/dist/string.d.ts +5 -1
  75. package/dist/string.d.ts.map +1 -1
  76. package/dist/string.js +106 -442
  77. package/dist/string.js.map +1 -1
  78. package/dist/string.test.d.ts +5 -0
  79. package/dist/string.test.d.ts.map +1 -0
  80. package/dist/time.cjs +512 -0
  81. package/dist/time.cjs.map +1 -0
  82. package/dist/time.d.ts.map +1 -1
  83. package/dist/time.js +41 -528
  84. package/dist/time.js.map +1 -1
  85. package/dist/traits/ARTraits.d.ts +14 -0
  86. package/dist/traits/ARTraits.d.ts.map +1 -0
  87. package/dist/traits/EconomicPrimitives.cjs +335 -0
  88. package/dist/traits/EconomicPrimitives.cjs.map +1 -0
  89. package/dist/traits/EconomicPrimitives.d.ts +345 -0
  90. package/dist/traits/EconomicPrimitives.d.ts.map +1 -0
  91. package/dist/traits/EconomicPrimitives.js +55 -0
  92. package/dist/traits/EconomicPrimitives.js.map +1 -0
  93. package/dist/traits/EconomicTraits.cjs +471 -0
  94. package/dist/traits/EconomicTraits.cjs.map +1 -0
  95. package/dist/traits/EconomicTraits.d.ts +71 -0
  96. package/dist/traits/EconomicTraits.d.ts.map +1 -0
  97. package/dist/traits/EconomicTraits.js +17 -0
  98. package/dist/traits/EconomicTraits.js.map +1 -0
  99. package/dist/traits/IoTTraits.d.ts +14 -0
  100. package/dist/traits/IoTTraits.d.ts.map +1 -0
  101. package/dist/traits/SimulationLabPrimitives.d.ts +187 -0
  102. package/dist/traits/SimulationLabPrimitives.d.ts.map +1 -0
  103. package/dist/traits/SimulationLabTraits.d.ts +42 -0
  104. package/dist/traits/SimulationLabTraits.d.ts.map +1 -0
  105. package/dist/traits/VRRTraits.d.ts +150 -0
  106. package/dist/traits/VRRTraits.d.ts.map +1 -0
  107. package/dist/traits/__tests__/ARTraits.test.d.ts +2 -0
  108. package/dist/traits/__tests__/ARTraits.test.d.ts.map +1 -0
  109. package/dist/traits/__tests__/EconomicPrimitives.test.d.ts +2 -0
  110. package/dist/traits/__tests__/EconomicPrimitives.test.d.ts.map +1 -0
  111. package/dist/traits/__tests__/EconomicTraits.test.d.ts +2 -0
  112. package/dist/traits/__tests__/EconomicTraits.test.d.ts.map +1 -0
  113. package/dist/types.d.ts +9 -0
  114. package/dist/types.d.ts.map +1 -1
  115. package/error-utf8.log +6 -0
  116. package/error.log +0 -0
  117. package/package.json +42 -19
  118. package/src/__tests__/EconomicPrimitives.test.ts +690 -0
  119. package/src/__tests__/EconomicTraits.test.ts +425 -0
  120. package/src/__tests__/SimulationLabPrimitives.test.ts +415 -0
  121. package/src/__tests__/collections.test.ts +182 -0
  122. package/src/__tests__/events.test.ts +135 -0
  123. package/src/__tests__/index.test.ts +14 -0
  124. package/src/__tests__/materials.test.ts +84 -0
  125. package/src/__tests__/math.test.ts +246 -0
  126. package/src/__tests__/physics.test.ts +83 -0
  127. package/src/__tests__/spatial.test.ts +333 -0
  128. package/src/__tests__/standard-library-fundamentals.test.ts +1301 -0
  129. package/src/__tests__/string-and-time-utilities.test.ts +1070 -0
  130. package/src/__tests__/string-decoupled.test.ts +14 -0
  131. package/src/__tests__/string.test.ts +164 -0
  132. package/src/__tests__/time.test.ts +110 -0
  133. package/src/collections.ts +8 -2
  134. package/src/events.ts +88 -0
  135. package/src/index.ts +161 -11
  136. package/src/materials.ts +109 -0
  137. package/src/math.ts +30 -14
  138. package/src/physics.ts +141 -0
  139. package/src/spatial.ts +320 -0
  140. package/src/string.basic.test.ts +14 -0
  141. package/src/string.test.ts +335 -0
  142. package/src/string.ts +45 -13
  143. package/src/time.ts +10 -11
  144. package/src/traits/ARTraits.ts +103 -0
  145. package/src/traits/EconomicPrimitives.ts +755 -0
  146. package/src/traits/EconomicTraits.ts +552 -0
  147. package/src/traits/IoTTraits.ts +102 -0
  148. package/src/traits/SimulationLabPrimitives.ts +650 -0
  149. package/src/traits/SimulationLabTraits.ts +191 -0
  150. package/src/traits/VRRTraits.ts +326 -0
  151. package/src/traits/__tests__/ARTraits.test.ts +14 -0
  152. package/src/traits/__tests__/EconomicPrimitives.test.ts +14 -0
  153. package/src/traits/__tests__/EconomicTraits.test.ts +14 -0
  154. package/src/types.ts +47 -12
  155. package/std_error.log +5 -0
  156. package/tsup.config.ts +17 -0
  157. package/vitest.config.ts +10 -0
  158. package/dist/types.js +0 -198
  159. 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('&lt;div&gt;test&lt;/div&gt;');
258
+ expect(escapeHtml('a & b')).toBe('a &amp; b');
259
+ expect(escapeHtml('"quoted"')).toBe('&quot;quoted&quot;');
260
+ });
261
+
262
+ it('unescapeHtml should unescape HTML entities', () => {
263
+ expect(unescapeHtml('&lt;div&gt;test&lt;/div&gt;')).toBe('<div>test</div>');
264
+ expect(unescapeHtml('a &amp; b')).toBe('a & b');
265
+ expect(unescapeHtml('&quot;quoted&quot;')).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.ceil(availableLength / 2);
120
- const rightLength = Math.floor(availableLength / 2);
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 += substring.length;
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
- const mins = Math.floor(ms / 60000);
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 `${hours}h ${mins}m`;
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
  '&#x27;': "'",
330
329
  '&#x2F;': '/',
331
330
  };
332
- return s.replace(/&(?:amp|lt|gt|quot|#39|#x27|#x2F);/g, (entity) => htmlEntities[entity] || entity);
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(length: number, charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'): string {
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 maxLen = Math.max(a.length, b.length);
497
- if (maxLen === 0) return 1;
498
- return 1 - levenshtein(a, b) / maxLen;
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
- lastCall = now();
127
- timeoutId = null;
128
- fn(...args);
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>(fn: () => T | Promise<T>): Promise<{ result: T; duration: number }> {
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
- console.log(`[${label}] ${duration.toFixed(2)}ms`);
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 ms = DateTime.diff(new Date(amount), new Date(0), 'ms');
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;