@tuturuuu/utils 0.0.3 → 0.6.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.
Files changed (186) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/biome.json +5 -0
  3. package/jsr.json +8 -8
  4. package/package.json +63 -32
  5. package/src/__tests__/ai-temp-auth.test.ts +309 -0
  6. package/src/__tests__/api-proxy-guard.test.ts +1451 -0
  7. package/src/__tests__/app-url.test.ts +270 -0
  8. package/src/__tests__/avatar-url.test.ts +97 -0
  9. package/src/__tests__/color-helper.test.ts +179 -0
  10. package/src/__tests__/constants.test.ts +351 -0
  11. package/src/__tests__/crypto.test.ts +107 -0
  12. package/src/__tests__/date-helper.test.ts +408 -0
  13. package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
  14. package/src/__tests__/format.test.ts +317 -0
  15. package/src/__tests__/html-sanitizer.test.ts +360 -0
  16. package/src/__tests__/interest-calculator.test.ts +336 -0
  17. package/src/__tests__/interest-detector.test.ts +222 -0
  18. package/src/__tests__/label-colors.test.ts +241 -0
  19. package/src/__tests__/name-helper.test.ts +158 -0
  20. package/src/__tests__/node-diff.test.ts +576 -0
  21. package/src/__tests__/notification-service.test.ts +210 -0
  22. package/src/__tests__/onboarding-helper.test.ts +331 -0
  23. package/src/__tests__/path-helper.test.ts +152 -0
  24. package/src/__tests__/permissions.test.tsx +81 -0
  25. package/src/__tests__/request-emoji-limit.test.ts +172 -0
  26. package/src/__tests__/search-helper.test.ts +51 -0
  27. package/src/__tests__/storage-display-name.test.ts +37 -0
  28. package/src/__tests__/storage-path.test.ts +238 -0
  29. package/src/__tests__/tag-utils.test.ts +205 -0
  30. package/src/__tests__/task-description-yjs-state.test.ts +581 -0
  31. package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
  32. package/src/__tests__/task-helper-create-task.test.ts +129 -0
  33. package/src/__tests__/task-helpers.test.ts +464 -0
  34. package/src/__tests__/task-overrides.test.ts +305 -0
  35. package/src/__tests__/task-reorder-cache.test.ts +74 -0
  36. package/src/__tests__/task-sort-keys.test.ts +36 -0
  37. package/src/__tests__/task-transformers.test.ts +62 -0
  38. package/src/__tests__/text-helper.test.ts +776 -0
  39. package/src/__tests__/time-helper.test.ts +70 -0
  40. package/src/__tests__/time-tracker-period.test.ts +55 -0
  41. package/src/__tests__/timezone.test.ts +117 -0
  42. package/src/__tests__/upstash-rest.test.ts +77 -0
  43. package/src/__tests__/uuid-helper.test.ts +133 -0
  44. package/src/__tests__/workspace-helper.test.ts +859 -0
  45. package/src/__tests__/workspace-limits.test.ts +255 -0
  46. package/src/__tests__/yjs-helper.test.ts +581 -0
  47. package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
  48. package/src/abuse-protection/__tests__/edge.test.ts +136 -0
  49. package/src/abuse-protection/__tests__/index.test.ts +562 -0
  50. package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
  51. package/src/abuse-protection/backend-rate-limit.ts +44 -0
  52. package/src/abuse-protection/constants.ts +117 -0
  53. package/src/abuse-protection/edge.ts +223 -0
  54. package/src/abuse-protection/index.ts +1545 -0
  55. package/src/abuse-protection/reputation.ts +587 -0
  56. package/src/abuse-protection/types.ts +97 -0
  57. package/src/abuse-protection/user-agent.ts +124 -0
  58. package/src/abuse-protection/user-suspension.ts +231 -0
  59. package/src/ai-temp-auth.ts +315 -0
  60. package/src/api-proxy-guard.ts +965 -0
  61. package/src/app-url.ts +96 -0
  62. package/src/avatar-url.ts +64 -0
  63. package/src/break-duration.ts +84 -0
  64. package/src/calendar-auth-token.test.ts +37 -0
  65. package/src/calendar-auth-token.ts +19 -0
  66. package/src/calendar-sync-coordination.md +197 -0
  67. package/src/calendar-utils.test.ts +169 -0
  68. package/src/calendar-utils.ts +91 -0
  69. package/src/color-helper.ts +110 -0
  70. package/src/common/nextjs.tsx +99 -0
  71. package/src/common/scan.tsx +15 -0
  72. package/src/configs/reports.ts +160 -0
  73. package/src/constants.ts +85 -0
  74. package/src/crypto.ts +21 -0
  75. package/src/currencies.ts +97 -0
  76. package/src/date-helper.ts +313 -0
  77. package/src/editor/convert-to-task.ts +264 -0
  78. package/src/editor/index.ts +5 -0
  79. package/src/email/__tests__/client.test.ts +141 -0
  80. package/src/email/__tests__/validation.test.ts +46 -0
  81. package/src/email/client.ts +92 -0
  82. package/src/email/server.ts +128 -0
  83. package/src/email/validation.ts +11 -0
  84. package/src/encryption/__tests__/calendar-events.test.ts +411 -0
  85. package/src/encryption/__tests__/configuration.test.ts +114 -0
  86. package/src/encryption/__tests__/field-encryption.test.ts +232 -0
  87. package/src/encryption/__tests__/key-generation.test.ts +30 -0
  88. package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
  89. package/src/encryption/__tests__/test-helpers.ts +22 -0
  90. package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
  91. package/src/encryption/encryption-service.ts +343 -0
  92. package/src/encryption/index.ts +25 -0
  93. package/src/encryption/types.ts +57 -0
  94. package/src/exchange-rates.ts +49 -0
  95. package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
  96. package/src/feature-flags/core.ts +322 -0
  97. package/src/feature-flags/data.ts +16 -0
  98. package/src/feature-flags/default.ts +18 -0
  99. package/src/feature-flags/index.ts +7 -0
  100. package/src/feature-flags/requestable-features.ts +79 -0
  101. package/src/feature-flags/types.ts +4 -0
  102. package/src/fetcher.ts +2 -0
  103. package/src/finance/index.ts +4 -0
  104. package/src/finance/interest-calculator.ts +456 -0
  105. package/src/finance/interest-detector.ts +141 -0
  106. package/src/finance/transform-invoice-results.ts +219 -0
  107. package/src/finance/wallet-permissions.test.ts +169 -0
  108. package/src/finance/wallet-permissions.ts +82 -0
  109. package/src/format.ts +120 -1
  110. package/src/generated/platform-build-metadata.ts +11 -0
  111. package/src/hooks/use-platform.ts +64 -0
  112. package/src/html-sanitizer.ts +155 -0
  113. package/src/internal-domains.ts +497 -0
  114. package/src/keyboard-preset.ts +109 -0
  115. package/src/label-colors.ts +213 -0
  116. package/src/launchable-apps.test.ts +126 -0
  117. package/src/launchable-apps.ts +490 -0
  118. package/src/name-helper.ts +269 -0
  119. package/src/next-config.test.ts +234 -0
  120. package/src/next-config.ts +203 -0
  121. package/src/node-diff.ts +375 -0
  122. package/src/notification-service.ts +379 -0
  123. package/src/nova/scores/__tests__/calculate.test.ts +254 -0
  124. package/src/nova/scores/calculate.ts +132 -0
  125. package/src/nova/submissions/check-permission.ts +132 -0
  126. package/src/onboarding-helper.ts +213 -0
  127. package/src/path-helper.ts +93 -0
  128. package/src/permissions.tsx +1170 -0
  129. package/src/plan-helpers.test.ts +188 -0
  130. package/src/plan-helpers.ts +80 -0
  131. package/src/platform-release.test.ts +74 -0
  132. package/src/platform-release.ts +155 -0
  133. package/src/portless.ts +124 -0
  134. package/src/priority-styles.ts +42 -0
  135. package/src/request-emoji-limit.ts +335 -0
  136. package/src/search-helper.ts +18 -0
  137. package/src/search.test.ts +89 -0
  138. package/src/search.ts +355 -0
  139. package/src/storage-display-name.ts +30 -0
  140. package/src/storage-path.ts +147 -0
  141. package/src/tag-utils.ts +159 -0
  142. package/src/task/reorder.ts +245 -0
  143. package/src/task/transformers.ts +149 -0
  144. package/src/task-date-timezone.ts +133 -0
  145. package/src/task-description-content.ts +240 -0
  146. package/src/task-helper/board.ts +193 -0
  147. package/src/task-helper/bulk-actions.ts +564 -0
  148. package/src/task-helper/personal-external-staging.ts +21 -0
  149. package/src/task-helper/recycle-bin.ts +202 -0
  150. package/src/task-helper/relationships.ts +346 -0
  151. package/src/task-helper/shared.ts +109 -0
  152. package/src/task-helper/sort-keys.ts +337 -0
  153. package/src/task-helper/task-hooks-basic.ts +342 -0
  154. package/src/task-helper/task-hooks-move.ts +264 -0
  155. package/src/task-helper/task-operations.ts +278 -0
  156. package/src/task-helper.ts +12 -0
  157. package/src/task-helpers.ts +241 -0
  158. package/src/task-list-status.ts +62 -0
  159. package/src/task-overrides.ts +82 -0
  160. package/src/task-snapshot.ts +374 -0
  161. package/src/text-diff.ts +81 -0
  162. package/src/text-helper.ts +537 -0
  163. package/src/time-helper.ts +63 -0
  164. package/src/time-tracker-period.ts +73 -0
  165. package/src/timeblock-helper.ts +418 -0
  166. package/src/timezone.ts +190 -0
  167. package/src/timezones.json +1271 -0
  168. package/src/upstash-rest.ts +56 -0
  169. package/src/user-helper.ts +296 -0
  170. package/src/uuid-helper.ts +11 -0
  171. package/src/workspace-handle.ts +10 -0
  172. package/src/workspace-helper.ts +1408 -0
  173. package/src/workspace-limits.ts +68 -0
  174. package/src/yjs-helper.ts +217 -0
  175. package/src/yjs-task-description.ts +81 -0
  176. package/tsconfig.json +3 -5
  177. package/tsconfig.typecheck.json +33 -0
  178. package/vitest.config.ts +36 -0
  179. package/dist/index.d.ts +0 -8
  180. package/dist/index.js +0 -2
  181. package/dist/index.js.map +0 -1
  182. package/dist/index.mjs +0 -2
  183. package/dist/index.mjs.map +0 -1
  184. package/eslint.config.mjs +0 -20
  185. package/rollup.config.js +0 -41
  186. package/src/index.ts +0 -1
@@ -0,0 +1,776 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ getDescriptionMetadata,
4
+ getDescriptionText,
5
+ getDescriptionTextWithIdentifiers,
6
+ removeAccents,
7
+ } from '../text-helper';
8
+
9
+ describe('Text Helper', () => {
10
+ describe('removeAccents', () => {
11
+ it('removes diacritics from accented characters', () => {
12
+ expect(removeAccents('café')).toBe('cafe');
13
+ expect(removeAccents('naïve')).toBe('naive');
14
+ expect(removeAccents('résumé')).toBe('resume');
15
+ });
16
+
17
+ it('handles Vietnamese characters', () => {
18
+ expect(removeAccents('đường')).toBe('duong');
19
+ expect(removeAccents('Đà Nẵng')).toBe('Da Nang');
20
+ });
21
+
22
+ it('replaces Vietnamese đ and Đ', () => {
23
+ expect(removeAccents('đ')).toBe('d');
24
+ expect(removeAccents('Đ')).toBe('D');
25
+ });
26
+
27
+ it('preserves unaccented characters', () => {
28
+ expect(removeAccents('hello')).toBe('hello');
29
+ expect(removeAccents('world')).toBe('world');
30
+ });
31
+
32
+ it('handles empty string', () => {
33
+ expect(removeAccents('')).toBe('');
34
+ });
35
+
36
+ it('handles mixed content', () => {
37
+ expect(removeAccents('Hello Việt Nam')).toBe('Hello Viet Nam');
38
+ });
39
+
40
+ it('handles numbers and symbols', () => {
41
+ expect(removeAccents('test123!@#')).toBe('test123!@#');
42
+ });
43
+ });
44
+
45
+ describe('getDescriptionText', () => {
46
+ it('returns empty string for empty input', () => {
47
+ expect(getDescriptionText('')).toBe('');
48
+ expect(getDescriptionText(undefined)).toBe('');
49
+ expect(getDescriptionText(null as any)).toBe('');
50
+ });
51
+
52
+ it('extracts text from simple paragraph', () => {
53
+ const json = JSON.stringify({
54
+ type: 'doc',
55
+ content: [
56
+ {
57
+ type: 'paragraph',
58
+ content: [{ type: 'text', text: 'Hello World' }],
59
+ },
60
+ ],
61
+ });
62
+ expect(getDescriptionText(json)).toBe('Hello World');
63
+ });
64
+
65
+ it('handles multiple paragraphs', () => {
66
+ const json = JSON.stringify({
67
+ type: 'doc',
68
+ content: [
69
+ {
70
+ type: 'paragraph',
71
+ content: [{ type: 'text', text: 'First paragraph' }],
72
+ },
73
+ {
74
+ type: 'paragraph',
75
+ content: [{ type: 'text', text: 'Second paragraph' }],
76
+ },
77
+ ],
78
+ });
79
+ const result = getDescriptionText(json);
80
+ expect(result).toContain('First paragraph');
81
+ expect(result).toContain('Second paragraph');
82
+ });
83
+
84
+ it('handles headings', () => {
85
+ const json = JSON.stringify({
86
+ type: 'doc',
87
+ content: [
88
+ {
89
+ type: 'heading',
90
+ attrs: { level: 1 },
91
+ content: [{ type: 'text', text: 'Main Title' }],
92
+ },
93
+ {
94
+ type: 'heading',
95
+ attrs: { level: 2 },
96
+ content: [{ type: 'text', text: 'Subtitle' }],
97
+ },
98
+ ],
99
+ });
100
+ const result = getDescriptionText(json);
101
+ expect(result).toContain('# Main Title');
102
+ expect(result).toContain('## Subtitle');
103
+ });
104
+
105
+ it('handles bullet lists', () => {
106
+ const json = JSON.stringify({
107
+ type: 'doc',
108
+ content: [
109
+ {
110
+ type: 'bulletList',
111
+ content: [
112
+ {
113
+ type: 'listItem',
114
+ content: [
115
+ {
116
+ type: 'paragraph',
117
+ content: [{ type: 'text', text: 'Item 1' }],
118
+ },
119
+ ],
120
+ },
121
+ {
122
+ type: 'listItem',
123
+ content: [
124
+ {
125
+ type: 'paragraph',
126
+ content: [{ type: 'text', text: 'Item 2' }],
127
+ },
128
+ ],
129
+ },
130
+ ],
131
+ },
132
+ ],
133
+ });
134
+ const result = getDescriptionText(json);
135
+ expect(result).toContain('Item 1');
136
+ expect(result).toContain('Item 2');
137
+ });
138
+
139
+ it('handles ordered lists', () => {
140
+ const json = JSON.stringify({
141
+ type: 'doc',
142
+ content: [
143
+ {
144
+ type: 'orderedList',
145
+ attrs: { start: 1 },
146
+ content: [
147
+ {
148
+ type: 'listItem',
149
+ content: [
150
+ {
151
+ type: 'paragraph',
152
+ content: [{ type: 'text', text: 'First' }],
153
+ },
154
+ ],
155
+ },
156
+ {
157
+ type: 'listItem',
158
+ content: [
159
+ {
160
+ type: 'paragraph',
161
+ content: [{ type: 'text', text: 'Second' }],
162
+ },
163
+ ],
164
+ },
165
+ ],
166
+ },
167
+ ],
168
+ });
169
+ const result = getDescriptionText(json);
170
+ expect(result).toContain('1.');
171
+ expect(result).toContain('2.');
172
+ });
173
+
174
+ it('handles task lists', () => {
175
+ const json = JSON.stringify({
176
+ type: 'doc',
177
+ content: [
178
+ {
179
+ type: 'taskList',
180
+ content: [
181
+ {
182
+ type: 'taskItem',
183
+ attrs: { checked: false },
184
+ content: [
185
+ {
186
+ type: 'paragraph',
187
+ content: [{ type: 'text', text: 'Todo item' }],
188
+ },
189
+ ],
190
+ },
191
+ {
192
+ type: 'taskItem',
193
+ attrs: { checked: true },
194
+ content: [
195
+ {
196
+ type: 'paragraph',
197
+ content: [{ type: 'text', text: 'Done item' }],
198
+ },
199
+ ],
200
+ },
201
+ ],
202
+ },
203
+ ],
204
+ });
205
+ const result = getDescriptionText(json);
206
+ expect(result).toContain('[ ] Todo item');
207
+ expect(result).toContain('[x] Done item');
208
+ });
209
+
210
+ it('handles blockquotes', () => {
211
+ const json = JSON.stringify({
212
+ type: 'doc',
213
+ content: [
214
+ {
215
+ type: 'blockquote',
216
+ content: [
217
+ {
218
+ type: 'paragraph',
219
+ content: [{ type: 'text', text: 'Quoted text' }],
220
+ },
221
+ ],
222
+ },
223
+ ],
224
+ });
225
+ const result = getDescriptionText(json);
226
+ expect(result).toContain('> Quoted text');
227
+ });
228
+
229
+ it('handles code blocks', () => {
230
+ const json = JSON.stringify({
231
+ type: 'doc',
232
+ content: [
233
+ {
234
+ type: 'codeBlock',
235
+ attrs: { language: 'javascript' },
236
+ content: [{ type: 'text', text: 'const x = 1;' }],
237
+ },
238
+ ],
239
+ });
240
+ const result = getDescriptionText(json);
241
+ expect(result).toContain('```javascript');
242
+ expect(result).toContain('const x = 1;');
243
+ expect(result).toContain('```');
244
+ });
245
+
246
+ it('handles hard breaks', () => {
247
+ const json = JSON.stringify({
248
+ type: 'doc',
249
+ content: [
250
+ {
251
+ type: 'paragraph',
252
+ content: [
253
+ { type: 'text', text: 'Line 1' },
254
+ { type: 'hardBreak' },
255
+ { type: 'text', text: 'Line 2' },
256
+ ],
257
+ },
258
+ ],
259
+ });
260
+ const result = getDescriptionText(json);
261
+ expect(result).toContain('Line 1\nLine 2');
262
+ });
263
+
264
+ it('handles horizontal rules', () => {
265
+ const json = JSON.stringify({
266
+ type: 'doc',
267
+ content: [
268
+ {
269
+ type: 'paragraph',
270
+ content: [{ type: 'text', text: 'Above' }],
271
+ },
272
+ { type: 'horizontalRule' },
273
+ {
274
+ type: 'paragraph',
275
+ content: [{ type: 'text', text: 'Below' }],
276
+ },
277
+ ],
278
+ });
279
+ const result = getDescriptionText(json);
280
+ expect(result).toContain('---');
281
+ });
282
+
283
+ it('handles images', () => {
284
+ const json = JSON.stringify({
285
+ type: 'doc',
286
+ content: [
287
+ {
288
+ type: 'image',
289
+ attrs: { alt: 'My Image' },
290
+ },
291
+ ],
292
+ });
293
+ const result = getDescriptionText(json);
294
+ expect(result).toContain('[My Image]');
295
+ });
296
+
297
+ it('handles mentions', () => {
298
+ const json = JSON.stringify({
299
+ type: 'doc',
300
+ content: [
301
+ {
302
+ type: 'paragraph',
303
+ content: [
304
+ { type: 'text', text: 'Hello ' },
305
+ { type: 'mention', attrs: { label: 'John' } },
306
+ ],
307
+ },
308
+ ],
309
+ });
310
+ const result = getDescriptionText(json);
311
+ expect(result).toContain('@John');
312
+ });
313
+
314
+ it('handles tables', () => {
315
+ const json = JSON.stringify({
316
+ type: 'doc',
317
+ content: [
318
+ {
319
+ type: 'table',
320
+ content: [
321
+ {
322
+ type: 'tableRow',
323
+ content: [
324
+ {
325
+ type: 'tableCell',
326
+ content: [
327
+ {
328
+ type: 'paragraph',
329
+ content: [{ type: 'text', text: 'Cell 1' }],
330
+ },
331
+ ],
332
+ },
333
+ {
334
+ type: 'tableCell',
335
+ content: [
336
+ {
337
+ type: 'paragraph',
338
+ content: [{ type: 'text', text: 'Cell 2' }],
339
+ },
340
+ ],
341
+ },
342
+ ],
343
+ },
344
+ ],
345
+ },
346
+ ],
347
+ });
348
+ const result = getDescriptionText(json);
349
+ expect(result).toContain('Cell 1');
350
+ expect(result).toContain('Cell 2');
351
+ expect(result).toContain('|');
352
+ });
353
+
354
+ it('handles YouTube embeds', () => {
355
+ const json = JSON.stringify({
356
+ type: 'doc',
357
+ content: [
358
+ { type: 'youtube', attrs: { src: 'https://youtube.com/...' } },
359
+ ],
360
+ });
361
+ const result = getDescriptionText(json);
362
+ expect(result).toContain('[YouTube Video]');
363
+ });
364
+
365
+ it('returns plain text for invalid JSON', () => {
366
+ expect(getDescriptionText('plain text')).toBe('plain text');
367
+ expect(getDescriptionText('not valid json {')).toBe('not valid json {');
368
+ });
369
+
370
+ it('handles Json object directly', () => {
371
+ const jsonObj = {
372
+ type: 'doc',
373
+ content: [
374
+ {
375
+ type: 'paragraph',
376
+ content: [{ type: 'text', text: 'Direct object' }],
377
+ },
378
+ ],
379
+ };
380
+ expect(getDescriptionText(jsonObj)).toBe('Direct object');
381
+ });
382
+
383
+ it('cleans up excessive newlines', () => {
384
+ const json = JSON.stringify({
385
+ type: 'doc',
386
+ content: [
387
+ { type: 'paragraph', content: [{ type: 'text', text: 'A' }] },
388
+ { type: 'paragraph', content: [] },
389
+ { type: 'paragraph', content: [] },
390
+ { type: 'paragraph', content: [] },
391
+ { type: 'paragraph', content: [{ type: 'text', text: 'B' }] },
392
+ ],
393
+ });
394
+ const result = getDescriptionText(json);
395
+ // Should not have more than 2 consecutive newlines
396
+ expect(result).not.toMatch(/\n{3,}/);
397
+ });
398
+ });
399
+
400
+ describe('getDescriptionMetadata', () => {
401
+ it('returns default metadata for empty input', () => {
402
+ const result = getDescriptionMetadata('');
403
+ expect(result.hasText).toBe(false);
404
+ expect(result.hasImages).toBe(false);
405
+ expect(result.hasVideos).toBe(false);
406
+ expect(result.hasLinks).toBe(false);
407
+ expect(result.imageCount).toBe(0);
408
+ expect(result.videoCount).toBe(0);
409
+ expect(result.linkCount).toBe(0);
410
+ expect(result.totalCheckboxes).toBe(0);
411
+ expect(result.checkedCheckboxes).toBe(0);
412
+ });
413
+
414
+ it('detects text content', () => {
415
+ const json = JSON.stringify({
416
+ type: 'doc',
417
+ content: [
418
+ {
419
+ type: 'paragraph',
420
+ content: [{ type: 'text', text: 'Hello' }],
421
+ },
422
+ ],
423
+ });
424
+ const result = getDescriptionMetadata(json);
425
+ expect(result.hasText).toBe(true);
426
+ });
427
+
428
+ it('ignores empty text', () => {
429
+ const json = JSON.stringify({
430
+ type: 'doc',
431
+ content: [
432
+ {
433
+ type: 'paragraph',
434
+ content: [{ type: 'text', text: ' ' }],
435
+ },
436
+ ],
437
+ });
438
+ const result = getDescriptionMetadata(json);
439
+ expect(result.hasText).toBe(false);
440
+ });
441
+
442
+ it('detects images', () => {
443
+ const json = JSON.stringify({
444
+ type: 'doc',
445
+ content: [
446
+ { type: 'image', attrs: { src: 'img1.png' } },
447
+ { type: 'image', attrs: { src: 'img2.png' } },
448
+ ],
449
+ });
450
+ const result = getDescriptionMetadata(json);
451
+ expect(result.hasImages).toBe(true);
452
+ expect(result.imageCount).toBe(2);
453
+ });
454
+
455
+ it('detects imageResize nodes', () => {
456
+ const json = JSON.stringify({
457
+ type: 'doc',
458
+ content: [{ type: 'imageResize', attrs: { src: 'img.png' } }],
459
+ });
460
+ const result = getDescriptionMetadata(json);
461
+ expect(result.hasImages).toBe(true);
462
+ expect(result.imageCount).toBe(1);
463
+ });
464
+
465
+ it('detects videos', () => {
466
+ const json = JSON.stringify({
467
+ type: 'doc',
468
+ content: [{ type: 'video', attrs: { src: 'video.mp4' } }],
469
+ });
470
+ const result = getDescriptionMetadata(json);
471
+ expect(result.hasVideos).toBe(true);
472
+ expect(result.videoCount).toBe(1);
473
+ });
474
+
475
+ it('detects YouTube embeds as videos', () => {
476
+ const json = JSON.stringify({
477
+ type: 'doc',
478
+ content: [
479
+ { type: 'youtube', attrs: { src: 'https://youtube.com/...' } },
480
+ { type: 'video', attrs: { src: 'video.mp4' } },
481
+ ],
482
+ });
483
+ const result = getDescriptionMetadata(json);
484
+ expect(result.hasVideos).toBe(true);
485
+ expect(result.videoCount).toBe(2);
486
+ });
487
+
488
+ it('detects links in marks', () => {
489
+ const json = JSON.stringify({
490
+ type: 'doc',
491
+ content: [
492
+ {
493
+ type: 'paragraph',
494
+ content: [
495
+ {
496
+ type: 'text',
497
+ text: 'Click here',
498
+ marks: [
499
+ { type: 'link', attrs: { href: 'https://example.com' } },
500
+ ],
501
+ },
502
+ ],
503
+ },
504
+ ],
505
+ });
506
+ const result = getDescriptionMetadata(json);
507
+ expect(result.hasLinks).toBe(true);
508
+ expect(result.linkCount).toBe(1);
509
+ });
510
+
511
+ it('counts checkboxes', () => {
512
+ const json = JSON.stringify({
513
+ type: 'doc',
514
+ content: [
515
+ {
516
+ type: 'taskList',
517
+ content: [
518
+ {
519
+ type: 'taskItem',
520
+ attrs: { checked: false },
521
+ content: [
522
+ {
523
+ type: 'paragraph',
524
+ content: [{ type: 'text', text: 'Todo' }],
525
+ },
526
+ ],
527
+ },
528
+ {
529
+ type: 'taskItem',
530
+ attrs: { checked: true },
531
+ content: [
532
+ {
533
+ type: 'paragraph',
534
+ content: [{ type: 'text', text: 'Done' }],
535
+ },
536
+ ],
537
+ },
538
+ {
539
+ type: 'taskItem',
540
+ attrs: { checked: true },
541
+ content: [
542
+ {
543
+ type: 'paragraph',
544
+ content: [{ type: 'text', text: 'Also done' }],
545
+ },
546
+ ],
547
+ },
548
+ ],
549
+ },
550
+ ],
551
+ });
552
+ const result = getDescriptionMetadata(json);
553
+ expect(result.totalCheckboxes).toBe(3);
554
+ expect(result.checkedCheckboxes).toBe(2);
555
+ });
556
+
557
+ it('handles invalid JSON as plain text', () => {
558
+ const result = getDescriptionMetadata('some plain text');
559
+ expect(result.hasText).toBe(true);
560
+ });
561
+
562
+ it('handles empty plain text', () => {
563
+ const result = getDescriptionMetadata(' ');
564
+ expect(result.hasText).toBe(false);
565
+ });
566
+
567
+ it('handles Json object directly', () => {
568
+ const jsonObj = {
569
+ type: 'doc',
570
+ content: [
571
+ {
572
+ type: 'paragraph',
573
+ content: [{ type: 'text', text: 'Direct object' }],
574
+ },
575
+ ],
576
+ };
577
+ const result = getDescriptionMetadata(jsonObj);
578
+ expect(result.hasText).toBe(true);
579
+ });
580
+
581
+ it('recursively analyzes nested content', () => {
582
+ const json = JSON.stringify({
583
+ type: 'doc',
584
+ content: [
585
+ {
586
+ type: 'blockquote',
587
+ content: [
588
+ {
589
+ type: 'paragraph',
590
+ content: [
591
+ { type: 'text', text: 'Nested text' },
592
+ { type: 'image', attrs: { src: 'img.png' } },
593
+ ],
594
+ },
595
+ ],
596
+ },
597
+ ],
598
+ });
599
+ const result = getDescriptionMetadata(json);
600
+ expect(result.hasText).toBe(true);
601
+ expect(result.hasImages).toBe(true);
602
+ });
603
+ });
604
+
605
+ describe('getDescriptionTextWithIdentifiers', () => {
606
+ it('returns empty string for empty input', () => {
607
+ expect(getDescriptionTextWithIdentifiers('')).toBe('');
608
+ expect(getDescriptionTextWithIdentifiers(undefined)).toBe('');
609
+ });
610
+
611
+ it('includes image filename in output', () => {
612
+ const json = JSON.stringify({
613
+ type: 'doc',
614
+ content: [
615
+ {
616
+ type: 'image',
617
+ attrs: { src: 'https://example.com/uploads/cat-photo.png' },
618
+ },
619
+ ],
620
+ });
621
+ const result = getDescriptionTextWithIdentifiers(json);
622
+ expect(result).toContain('[Image: cat-photo.png]');
623
+ });
624
+
625
+ it('includes image alt and filename', () => {
626
+ const json = JSON.stringify({
627
+ type: 'doc',
628
+ content: [
629
+ {
630
+ type: 'image',
631
+ attrs: { src: 'https://example.com/cat.png', alt: 'My Cat' },
632
+ },
633
+ ],
634
+ });
635
+ const result = getDescriptionTextWithIdentifiers(json);
636
+ expect(result).toContain('[Image: My Cat (cat.png)]');
637
+ });
638
+
639
+ it('falls back to alt text when no src', () => {
640
+ const json = JSON.stringify({
641
+ type: 'doc',
642
+ content: [
643
+ {
644
+ type: 'image',
645
+ attrs: { alt: 'My Image' },
646
+ },
647
+ ],
648
+ });
649
+ const result = getDescriptionTextWithIdentifiers(json);
650
+ expect(result).toContain('[Image: My Image]');
651
+ });
652
+
653
+ it('includes video filename in output', () => {
654
+ const json = JSON.stringify({
655
+ type: 'doc',
656
+ content: [
657
+ {
658
+ type: 'video',
659
+ attrs: { src: 'https://example.com/uploads/intro.mp4' },
660
+ },
661
+ ],
662
+ });
663
+ const result = getDescriptionTextWithIdentifiers(json);
664
+ expect(result).toContain('[Video: intro.mp4]');
665
+ });
666
+
667
+ it('extracts YouTube video ID', () => {
668
+ const json = JSON.stringify({
669
+ type: 'doc',
670
+ content: [
671
+ {
672
+ type: 'youtube',
673
+ attrs: { src: 'https://youtube.com/watch?v=abc123' },
674
+ },
675
+ ],
676
+ });
677
+ const result = getDescriptionTextWithIdentifiers(json);
678
+ expect(result).toContain('[YouTube: abc123]');
679
+ });
680
+
681
+ it('handles imageResize nodes', () => {
682
+ const json = JSON.stringify({
683
+ type: 'doc',
684
+ content: [
685
+ {
686
+ type: 'imageResize',
687
+ attrs: { src: 'https://example.com/resized.png', width: 400 },
688
+ },
689
+ ],
690
+ });
691
+ const result = getDescriptionTextWithIdentifiers(json);
692
+ expect(result).toContain('[Image: resized.png]');
693
+ });
694
+
695
+ it('truncates long filenames', () => {
696
+ const json = JSON.stringify({
697
+ type: 'doc',
698
+ content: [
699
+ {
700
+ type: 'image',
701
+ attrs: {
702
+ src: 'https://example.com/very-long-filename-that-should-be-truncated.png',
703
+ },
704
+ },
705
+ ],
706
+ });
707
+ const result = getDescriptionTextWithIdentifiers(json);
708
+ expect(result).toContain('...');
709
+ expect(result.length).toBeLessThan(80);
710
+ });
711
+
712
+ it('includes mention ID when different from label', () => {
713
+ const json = JSON.stringify({
714
+ type: 'doc',
715
+ content: [
716
+ {
717
+ type: 'paragraph',
718
+ content: [
719
+ {
720
+ type: 'mention',
721
+ attrs: { id: 'user-12345678-abcd', label: 'John' },
722
+ },
723
+ ],
724
+ },
725
+ ],
726
+ });
727
+ const result = getDescriptionTextWithIdentifiers(json);
728
+ expect(result).toContain('@John#user-123');
729
+ });
730
+
731
+ it('detects different images with same alt text', () => {
732
+ const json1 = JSON.stringify({
733
+ type: 'doc',
734
+ content: [{ type: 'image', attrs: { src: 'cat.png', alt: 'Pet' } }],
735
+ });
736
+ const json2 = JSON.stringify({
737
+ type: 'doc',
738
+ content: [{ type: 'image', attrs: { src: 'dog.png', alt: 'Pet' } }],
739
+ });
740
+ const result1 = getDescriptionTextWithIdentifiers(json1);
741
+ const result2 = getDescriptionTextWithIdentifiers(json2);
742
+ // Should produce different outputs even with same alt
743
+ expect(result1).not.toEqual(result2);
744
+ expect(result1).toContain('cat.png');
745
+ expect(result2).toContain('dog.png');
746
+ });
747
+
748
+ it('handles regular text normally', () => {
749
+ const json = JSON.stringify({
750
+ type: 'doc',
751
+ content: [
752
+ {
753
+ type: 'paragraph',
754
+ content: [{ type: 'text', text: 'Hello World' }],
755
+ },
756
+ ],
757
+ });
758
+ const result = getDescriptionTextWithIdentifiers(json);
759
+ expect(result).toBe('Hello World');
760
+ });
761
+
762
+ it('handles Json object directly', () => {
763
+ const jsonObj = {
764
+ type: 'doc',
765
+ content: [
766
+ {
767
+ type: 'image',
768
+ attrs: { src: 'test.png' },
769
+ },
770
+ ],
771
+ };
772
+ const result = getDescriptionTextWithIdentifiers(jsonObj);
773
+ expect(result).toContain('[Image: test.png]');
774
+ });
775
+ });
776
+ });