@atproto/api 0.20.22 β†’ 0.20.25

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 (64) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/client/lexicons.d.ts +28 -12
  3. package/dist/client/lexicons.d.ts.map +1 -1
  4. package/dist/client/lexicons.js +14 -6
  5. package/dist/client/lexicons.js.map +1 -1
  6. package/dist/client/types/app/bsky/ageassurance/defs.d.ts +2 -0
  7. package/dist/client/types/app/bsky/ageassurance/defs.d.ts.map +1 -1
  8. package/dist/client/types/app/bsky/ageassurance/defs.js.map +1 -1
  9. package/package.json +22 -16
  10. package/definitions/labels.json +0 -170
  11. package/docs/moderation.md +0 -260
  12. package/jest.config.cjs +0 -23
  13. package/jest.d.ts +0 -20
  14. package/jest.setup.ts +0 -97
  15. package/scripts/code/labels.mjs +0 -77
  16. package/scripts/generate-code.mjs +0 -4
  17. package/src/age-assurance.test.ts +0 -218
  18. package/src/age-assurance.ts +0 -137
  19. package/src/agent.ts +0 -1645
  20. package/src/atp-agent.ts +0 -570
  21. package/src/bsky-agent.ts +0 -14
  22. package/src/const.ts +0 -1
  23. package/src/index.ts +0 -44
  24. package/src/mocker.ts +0 -220
  25. package/src/moderation/decision.ts +0 -389
  26. package/src/moderation/index.ts +0 -69
  27. package/src/moderation/mutewords.ts +0 -170
  28. package/src/moderation/subjects/account.ts +0 -44
  29. package/src/moderation/subjects/feed-generator.ts +0 -24
  30. package/src/moderation/subjects/notification.ts +0 -25
  31. package/src/moderation/subjects/post.ts +0 -433
  32. package/src/moderation/subjects/profile.ts +0 -26
  33. package/src/moderation/subjects/status.ts +0 -27
  34. package/src/moderation/subjects/user-list.ts +0 -48
  35. package/src/moderation/types.ts +0 -191
  36. package/src/moderation/ui.ts +0 -21
  37. package/src/moderation/util.ts +0 -111
  38. package/src/predicate.ts +0 -52
  39. package/src/rich-text/detection.ts +0 -146
  40. package/src/rich-text/rich-text.ts +0 -418
  41. package/src/rich-text/sanitization.ts +0 -42
  42. package/src/rich-text/unicode.ts +0 -47
  43. package/src/rich-text/util.ts +0 -15
  44. package/src/session-manager.ts +0 -5
  45. package/src/types.ts +0 -165
  46. package/src/util.ts +0 -96
  47. package/tests/atp-agent.test.ts +0 -3863
  48. package/tests/dispatcher.test.ts +0 -527
  49. package/tests/errors.test.ts +0 -29
  50. package/tests/moderation-behaviors.test.ts +0 -951
  51. package/tests/moderation-custom-labels.test.ts +0 -334
  52. package/tests/moderation-mutewords.test.ts +0 -1299
  53. package/tests/moderation-prefs.test.ts +0 -402
  54. package/tests/moderation-quoteposts.test.ts +0 -277
  55. package/tests/moderation.test.ts +0 -739
  56. package/tests/rich-text-detection.test.ts +0 -456
  57. package/tests/rich-text-sanitization.test.ts +0 -216
  58. package/tests/rich-text.test.ts +0 -705
  59. package/tests/util/echo-server.ts +0 -37
  60. package/tests/util/moderation-behavior.ts +0 -268
  61. package/tsconfig.build.json +0 -9
  62. package/tsconfig.build.tsbuildinfo +0 -1
  63. package/tsconfig.json +0 -7
  64. package/tsconfig.tests.json +0 -10
@@ -1,456 +0,0 @@
1
- import {
2
- isLink,
3
- isMention,
4
- isTag,
5
- } from '../src/client/types/app/bsky/richtext/facet.js'
6
- import { AtpAgent, RichText, RichTextSegment } from '../src/index.js'
7
-
8
- describe('detectFacets', () => {
9
- const agent = new AtpAgent({ service: 'http://localhost' })
10
-
11
- // Mock handle resolution
12
- agent.com.atproto.identity.resolveHandle = async (params) => ({
13
- success: true,
14
- headers: {},
15
- data: { did: `did:fake:${params?.handle}` },
16
- })
17
-
18
- const inputs = [
19
- 'no mention',
20
- '@handle.com middle end',
21
- 'start @handle.com end',
22
- 'start middle @handle.com',
23
- '@handle.com @handle.com @handle.com',
24
- '@full123-chars.test',
25
- 'not@right',
26
- '@handle.com!@#$chars',
27
- '@handle.com\n@handle.com',
28
- 'parenthetical (@handle.com)',
29
- 'πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ @handle.com πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§',
30
-
31
- 'start https://middle.com end',
32
- 'start https://middle.com/foo/bar end',
33
- 'start https://middle.com/foo/bar?baz=bux end',
34
- 'start https://middle.com/foo/bar?baz=bux#hash end',
35
- 'https://start.com/foo/bar?baz=bux#hash middle end',
36
- 'start middle https://end.com/foo/bar?baz=bux#hash',
37
- 'https://newline1.com\nhttps://newline2.com',
38
- 'πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ https://middle.com πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§',
39
-
40
- 'start middle.com end',
41
- 'start middle.com/foo/bar end',
42
- 'start middle.com/foo/bar?baz=bux end',
43
- 'start middle.com/foo/bar?baz=bux#hash end',
44
- 'start.com/foo/bar?baz=bux#hash middle end',
45
- 'start middle end.com/foo/bar?baz=bux#hash',
46
- 'newline1.com\nnewline2.com',
47
- 'a example.com/index.php php link',
48
- 'a trailing bsky.app: colon',
49
-
50
- 'not.. a..url ..here',
51
- 'e.g.',
52
- 'something-cool.jpg',
53
- 'website.com.jpg',
54
- 'e.g./foo',
55
- 'website.com.jpg/foo',
56
-
57
- 'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
58
- 'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/ ',
59
- 'https://foo.com https://bar.com/whatever https://baz.com',
60
- 'punctuation https://foo.com, https://bar.com/whatever; https://baz.com.',
61
- 'parenthentical (https://foo.com)',
62
- 'except for https://foo.com/thing_(cool)',
63
- ]
64
- const outputs: string[][][] = [
65
- [['no mention']],
66
- [['@handle.com', 'did:fake:handle.com'], [' middle end']],
67
- [['start '], ['@handle.com', 'did:fake:handle.com'], [' end']],
68
- [['start middle '], ['@handle.com', 'did:fake:handle.com']],
69
- [
70
- ['@handle.com', 'did:fake:handle.com'],
71
- [' '],
72
- ['@handle.com', 'did:fake:handle.com'],
73
- [' '],
74
- ['@handle.com', 'did:fake:handle.com'],
75
- ],
76
- [['@full123-chars.test', 'did:fake:full123-chars.test']],
77
- [['not@right']],
78
- [['@handle.com', 'did:fake:handle.com'], ['!@#$chars']],
79
- [
80
- ['@handle.com', 'did:fake:handle.com'],
81
- ['\n'],
82
- ['@handle.com', 'did:fake:handle.com'],
83
- ],
84
- [['parenthetical ('], ['@handle.com', 'did:fake:handle.com'], [')']],
85
- [['πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ '], ['@handle.com', 'did:fake:handle.com'], [' πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§']],
86
-
87
- [['start '], ['https://middle.com', 'https://middle.com'], [' end']],
88
- [
89
- ['start '],
90
- ['https://middle.com/foo/bar', 'https://middle.com/foo/bar'],
91
- [' end'],
92
- ],
93
- [
94
- ['start '],
95
- [
96
- 'https://middle.com/foo/bar?baz=bux',
97
- 'https://middle.com/foo/bar?baz=bux',
98
- ],
99
- [' end'],
100
- ],
101
- [
102
- ['start '],
103
- [
104
- 'https://middle.com/foo/bar?baz=bux#hash',
105
- 'https://middle.com/foo/bar?baz=bux#hash',
106
- ],
107
- [' end'],
108
- ],
109
- [
110
- [
111
- 'https://start.com/foo/bar?baz=bux#hash',
112
- 'https://start.com/foo/bar?baz=bux#hash',
113
- ],
114
- [' middle end'],
115
- ],
116
- [
117
- ['start middle '],
118
- [
119
- 'https://end.com/foo/bar?baz=bux#hash',
120
- 'https://end.com/foo/bar?baz=bux#hash',
121
- ],
122
- ],
123
- [
124
- ['https://newline1.com', 'https://newline1.com'],
125
- ['\n'],
126
- ['https://newline2.com', 'https://newline2.com'],
127
- ],
128
- [['πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ '], ['https://middle.com', 'https://middle.com'], [' πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§']],
129
-
130
- [['start '], ['middle.com', 'https://middle.com'], [' end']],
131
- [
132
- ['start '],
133
- ['middle.com/foo/bar', 'https://middle.com/foo/bar'],
134
- [' end'],
135
- ],
136
- [
137
- ['start '],
138
- ['middle.com/foo/bar?baz=bux', 'https://middle.com/foo/bar?baz=bux'],
139
- [' end'],
140
- ],
141
- [
142
- ['start '],
143
- [
144
- 'middle.com/foo/bar?baz=bux#hash',
145
- 'https://middle.com/foo/bar?baz=bux#hash',
146
- ],
147
- [' end'],
148
- ],
149
- [
150
- [
151
- 'start.com/foo/bar?baz=bux#hash',
152
- 'https://start.com/foo/bar?baz=bux#hash',
153
- ],
154
- [' middle end'],
155
- ],
156
- [
157
- ['start middle '],
158
- ['end.com/foo/bar?baz=bux#hash', 'https://end.com/foo/bar?baz=bux#hash'],
159
- ],
160
- [
161
- ['newline1.com', 'https://newline1.com'],
162
- ['\n'],
163
- ['newline2.com', 'https://newline2.com'],
164
- ],
165
- [
166
- ['a '],
167
- ['example.com/index.php', 'https://example.com/index.php'],
168
- [' php link'],
169
- ],
170
- [['a trailing '], ['bsky.app', 'https://bsky.app'], [': colon']],
171
-
172
- [['not.. a..url ..here']],
173
- [['e.g.']],
174
- [['something-cool.jpg']],
175
- [['website.com.jpg']],
176
- [['e.g./foo']],
177
- [['website.com.jpg/foo']],
178
-
179
- [
180
- ['Classic article '],
181
- [
182
- 'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
183
- 'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
184
- ],
185
- ],
186
- [
187
- ['Classic article '],
188
- [
189
- 'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
190
- 'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
191
- ],
192
- [' '],
193
- ],
194
- [
195
- ['https://foo.com', 'https://foo.com'],
196
- [' '],
197
- ['https://bar.com/whatever', 'https://bar.com/whatever'],
198
- [' '],
199
- ['https://baz.com', 'https://baz.com'],
200
- ],
201
- [
202
- ['punctuation '],
203
- ['https://foo.com', 'https://foo.com'],
204
- [', '],
205
- ['https://bar.com/whatever', 'https://bar.com/whatever'],
206
- ['; '],
207
- ['https://baz.com', 'https://baz.com'],
208
- ['.'],
209
- ],
210
- [['parenthentical ('], ['https://foo.com', 'https://foo.com'], [')']],
211
- [
212
- ['except for '],
213
- ['https://foo.com/thing_(cool)', 'https://foo.com/thing_(cool)'],
214
- ],
215
- ]
216
- it('correctly handles a set of text inputs', async () => {
217
- for (let i = 0; i < inputs.length; i++) {
218
- const input = inputs[i]
219
- const rt = new RichText({ text: input })
220
- await rt.detectFacets(agent)
221
- expect(Array.from(rt.segments(), segmentToOutput)).toEqual(outputs[i])
222
- }
223
- })
224
-
225
- describe('correctly detects tags inline', () => {
226
- const inputs: [
227
- string,
228
- string[],
229
- { byteStart: number; byteEnd: number }[],
230
- ][] = [
231
- ['#a', ['a'], [{ byteStart: 0, byteEnd: 2 }]],
232
- [
233
- '#a #b',
234
- ['a', 'b'],
235
- [
236
- { byteStart: 0, byteEnd: 2 },
237
- { byteStart: 3, byteEnd: 5 },
238
- ],
239
- ],
240
- ['#1', [], []],
241
- ['#1a', ['1a'], [{ byteStart: 0, byteEnd: 3 }]],
242
- ['#tag', ['tag'], [{ byteStart: 0, byteEnd: 4 }]],
243
- ['body #tag', ['tag'], [{ byteStart: 5, byteEnd: 9 }]],
244
- ['#tag body', ['tag'], [{ byteStart: 0, byteEnd: 4 }]],
245
- ['body #tag body', ['tag'], [{ byteStart: 5, byteEnd: 9 }]],
246
- ['body #1', [], []],
247
- ['body #1a', ['1a'], [{ byteStart: 5, byteEnd: 8 }]],
248
- ['body #a1', ['a1'], [{ byteStart: 5, byteEnd: 8 }]],
249
- ['#', [], []],
250
- ['#?', [], []],
251
- ['text #', [], []],
252
- ['text # text', [], []],
253
- [
254
- 'body #thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
255
- ['thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'],
256
- [{ byteStart: 5, byteEnd: 70 }],
257
- ],
258
- [
259
- 'body #thisisa65characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab',
260
- [],
261
- [],
262
- ],
263
- [
264
- 'body #thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!',
265
- ['thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'],
266
- [{ byteStart: 5, byteEnd: 70 }],
267
- ],
268
- [
269
- 'its a #double#rainbow',
270
- ['double#rainbow'],
271
- [{ byteStart: 6, byteEnd: 21 }],
272
- ],
273
- ['##hashash', ['#hashash'], [{ byteStart: 0, byteEnd: 9 }]],
274
- ['##', [], []],
275
- ['some #n0n3s@n5e!', ['n0n3s@n5e'], [{ byteStart: 5, byteEnd: 15 }]],
276
- [
277
- 'works #with,punctuation',
278
- ['with,punctuation'],
279
- [{ byteStart: 6, byteEnd: 23 }],
280
- ],
281
- [
282
- 'strips trailing #punctuation, #like. #this!',
283
- ['punctuation', 'like', 'this'],
284
- [
285
- { byteStart: 16, byteEnd: 28 },
286
- { byteStart: 30, byteEnd: 35 },
287
- { byteStart: 37, byteEnd: 42 },
288
- ],
289
- ],
290
- [
291
- 'strips #multi_trailing___...',
292
- ['multi_trailing'],
293
- [{ byteStart: 7, byteEnd: 22 }],
294
- ],
295
- [
296
- 'works with #πŸ¦‹ emoji, and #butterπŸ¦‹fly',
297
- ['πŸ¦‹', 'butterπŸ¦‹fly'],
298
- [
299
- { byteStart: 11, byteEnd: 16 },
300
- { byteStart: 28, byteEnd: 42 },
301
- ],
302
- ],
303
- [
304
- '#same #same #but #diff',
305
- ['same', 'same', 'but', 'diff'],
306
- [
307
- { byteStart: 0, byteEnd: 5 },
308
- { byteStart: 6, byteEnd: 11 },
309
- { byteStart: 12, byteEnd: 16 },
310
- { byteStart: 17, byteEnd: 22 },
311
- ],
312
- ],
313
- ['this #️⃣tag should not be a tag', [], []],
314
- [
315
- 'this ##️⃣tag should be a tag',
316
- ['#️⃣tag'],
317
- [
318
- {
319
- byteStart: 5,
320
- byteEnd: 16,
321
- },
322
- ],
323
- ],
324
- [
325
- 'this #t\nag should be a tag',
326
- ['t'],
327
- [
328
- {
329
- byteStart: 5,
330
- byteEnd: 7,
331
- },
332
- ],
333
- ],
334
- ['no match (\\u200B): #​', [], []],
335
- ['no match (\\u200Ba): #​a', [], []],
336
- ['match (a\\u200Bb): #a​b', ['a'], [{ byteStart: 18, byteEnd: 20 }]],
337
- ['match (ab\\u200B): #ab​', ['ab'], [{ byteStart: 18, byteEnd: 21 }]],
338
- ['no match (\\u20e2tag): #βƒ’tag', [], []],
339
- ['no match (a\\u20e2b): #a⃒b', ['a'], [{ byteStart: 21, byteEnd: 23 }]],
340
- [
341
- 'match full width number sign (tag): οΌƒtag',
342
- ['tag'],
343
- [{ byteStart: 36, byteEnd: 42 }],
344
- ],
345
- [
346
- 'match full width number sign (tag): οΌƒ#️⃣tag',
347
- ['#️⃣tag'],
348
- [{ byteStart: 36, byteEnd: 49 }],
349
- ],
350
- ['no match 1?: #1?', [], []],
351
- ]
352
-
353
- it.each(inputs)('%s', async (input, tags, indices) => {
354
- const rt = new RichText({ text: input })
355
- await rt.detectFacets(agent)
356
-
357
- const detectedTags: string[] = []
358
- const detectedIndices: { byteStart: number; byteEnd: number }[] = []
359
-
360
- for (const { facet } of rt.segments()) {
361
- if (!facet) continue
362
- for (const feature of facet.features) {
363
- if (isTag(feature)) {
364
- detectedTags.push(feature.tag)
365
- }
366
- }
367
- detectedIndices.push(facet.index)
368
- }
369
-
370
- expect(detectedTags).toEqual(tags)
371
- expect(detectedIndices).toEqual(indices)
372
- })
373
- })
374
-
375
- describe('correctly detects cashtags inline', () => {
376
- const inputs: [
377
- string,
378
- string[],
379
- { byteStart: number; byteEnd: number }[],
380
- ][] = [
381
- ['$AAPL', ['$AAPL'], [{ byteStart: 0, byteEnd: 5 }]],
382
- ['$aapl', ['$AAPL'], [{ byteStart: 0, byteEnd: 5 }]], // normalized to uppercase
383
- ['$A', ['$A'], [{ byteStart: 0, byteEnd: 2 }]],
384
- ['$a', ['$A'], [{ byteStart: 0, byteEnd: 2 }]], // single char normalized
385
- [
386
- '$BTC $ETH',
387
- ['$BTC', '$ETH'],
388
- [
389
- { byteStart: 0, byteEnd: 4 },
390
- { byteStart: 5, byteEnd: 9 },
391
- ],
392
- ],
393
- ['$100', [], []], // starts with digit - not a cashtag
394
- ['$GOOGL', ['$GOOGL'], [{ byteStart: 0, byteEnd: 6 }]], // 5 chars - max length
395
- ['$TOOLONG', [], []], // >5 chars
396
- ['check $LEGO now', ['$LEGO'], [{ byteStart: 6, byteEnd: 11 }]],
397
- ['($GOOG)', ['$GOOG'], [{ byteStart: 1, byteEnd: 6 }]],
398
- ['$AAPL.', ['$AAPL'], [{ byteStart: 0, byteEnd: 5 }]], // trailing punctuation
399
- [
400
- '$AAPL, $MSFT!',
401
- ['$AAPL', '$MSFT'],
402
- [
403
- { byteStart: 0, byteEnd: 5 },
404
- { byteStart: 7, byteEnd: 12 },
405
- ],
406
- ],
407
- ['no$SPACE', [], []], // must have leading space or start
408
- ['$', [], []], // just dollar sign
409
- ['$ AAPL', [], []], // space after $
410
- ['$123ABC', [], []], // starts with digit
411
- ['$ABC12', ['$ABC12'], [{ byteStart: 0, byteEnd: 6 }]], // digits after letters OK (5 chars)
412
- ['$ABC123', [], []], // 6 chars - too long
413
- ]
414
-
415
- it.each(inputs)('%s', (input, tags, indices) => {
416
- const rt = new RichText({ text: input })
417
- rt.detectFacetsWithoutResolution()
418
-
419
- const detectedTags: string[] = []
420
- const detectedIndices: { byteStart: number; byteEnd: number }[] = []
421
-
422
- for (const { facet } of rt.segments()) {
423
- if (!facet) continue
424
- for (const feature of facet.features) {
425
- if (isTag(feature) && feature.tag.startsWith('$')) {
426
- detectedTags.push(feature.tag)
427
- }
428
- }
429
- if (
430
- facet.features.some(
431
- (f) => isTag(f) && (f as any).tag?.startsWith('$'),
432
- )
433
- ) {
434
- detectedIndices.push(facet.index)
435
- }
436
- }
437
-
438
- expect(detectedTags).toEqual(tags)
439
- expect(detectedIndices).toEqual(indices)
440
- })
441
- })
442
- })
443
-
444
- function segmentToOutput(segment: RichTextSegment): string[] {
445
- if (segment.facet) {
446
- return [
447
- segment.text,
448
- segment.facet?.features.map((f) => {
449
- if (isMention(f)) return f.did
450
- if (isLink(f)) return f.uri
451
- return undefined
452
- })?.[0] || '',
453
- ]
454
- }
455
- return [segment.text]
456
- }
@@ -1,216 +0,0 @@
1
- import {
2
- Facet,
3
- RichText,
4
- UnicodeString,
5
- sanitizeRichText,
6
- } from '../src/index.js'
7
-
8
- describe('sanitizeRichText: cleanNewlines', () => {
9
- it('removes more than two consecutive new lines', () => {
10
- const input = new RichText({
11
- text: 'test\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest',
12
- })
13
- const output = sanitizeRichText(input, { cleanNewlines: true })
14
- expect(String(output.unicodeText)).toEqual(
15
- 'test\n\ntest\n\ntest\n\ntest\n\ntest',
16
- )
17
- })
18
-
19
- it('removes more than two consecutive new lines w/fat unicode', () => {
20
- const input = new RichText({
21
- text: 'testπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§\n\n\n\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\n\n\n\n\n\ntestπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§\n\n\n\n\n\n\ntest\n\n\n\n\n\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test',
22
- })
23
- const output = sanitizeRichText(input, { cleanNewlines: true })
24
- expect(String(output.unicodeText)).toEqual(
25
- 'testπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\ntestπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§\n\ntest\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test',
26
- )
27
- })
28
-
29
- it('removes more than two consecutive new lines with spaces', () => {
30
- const input = new RichText({
31
- text: 'test\n\n\n\n\ntest\n \n \n \n \n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n \n\ntest',
32
- })
33
- const output = sanitizeRichText(input, { cleanNewlines: true })
34
- expect(String(output.unicodeText)).toEqual(
35
- 'test\n\ntest\n\ntest\n\ntest\n\ntest',
36
- )
37
- })
38
-
39
- it('returns original string if there are no consecutive new lines', () => {
40
- const input = new RichText({ text: 'test\n\ntest\n\ntest\n\ntest\n\ntest' })
41
- const output = sanitizeRichText(input, { cleanNewlines: true })
42
- expect(String(output.unicodeText)).toEqual(String(input.unicodeText))
43
- })
44
-
45
- it('returns original string if there are no new lines', () => {
46
- const input = new RichText({ text: 'test test test test test' })
47
- const output = sanitizeRichText(input, { cleanNewlines: true })
48
- expect(String(output.unicodeText)).toEqual(String(input.unicodeText))
49
- })
50
-
51
- it('returns empty string if input is empty', () => {
52
- const input = new RichText({ text: '' })
53
- const output = sanitizeRichText(input, { cleanNewlines: true })
54
- expect(String(output.unicodeText)).toEqual('')
55
- })
56
-
57
- it('works with different types of new line characters', () => {
58
- const input = new RichText({
59
- text: 'test\r\ntest\n\rtest\rtest\n\n\n\ntest\n\r \n \n \n \n\n\ntest',
60
- })
61
- const output = sanitizeRichText(input, { cleanNewlines: true })
62
- expect(String(output.unicodeText)).toEqual(
63
- 'test\r\ntest\n\rtest\rtest\n\ntest\n\ntest',
64
- )
65
- })
66
-
67
- it('removes more than two consecutive new lines with zero width space', () => {
68
- const input = new RichText({
69
- text: 'test\n\n\n\n\ntest\n\u200B\u200B\n\n\n\ntest\n \u200B\u200B \n\n\n\ntest\n\n\n\n\n\n\ntest',
70
- })
71
- const output = sanitizeRichText(input, { cleanNewlines: true })
72
- expect(String(output.unicodeText)).toEqual(
73
- 'test\n\ntest\n\ntest\n\ntest\n\ntest',
74
- )
75
- })
76
-
77
- it('removes more than two consecutive new lines with zero width non-joiner', () => {
78
- const input = new RichText({
79
- text: 'test\n\n\n\n\ntest\n\u200C\u200C\n\n\n\ntest\n \u200C\u200C \n\n\n\ntest\n\n\n\n\n\n\ntest',
80
- })
81
- const output = sanitizeRichText(input, { cleanNewlines: true })
82
- expect(String(output.unicodeText)).toEqual(
83
- 'test\n\ntest\n\ntest\n\ntest\n\ntest',
84
- )
85
- })
86
-
87
- it('removes more than two consecutive new lines with zero width joiner', () => {
88
- const input = new RichText({
89
- text: 'test\n\n\n\n\ntest\n\u200D\u200D\n\n\n\ntest\n \u200D\u200D \n\n\n\ntest\n\n\n\n\n\n\ntest',
90
- })
91
- const output = sanitizeRichText(input, { cleanNewlines: true })
92
- expect(String(output.unicodeText)).toEqual(
93
- 'test\n\ntest\n\ntest\n\ntest\n\ntest',
94
- )
95
- })
96
-
97
- it('removes more than two consecutive new lines with soft hyphen', () => {
98
- const input = new RichText({
99
- text: 'test\n\n\n\n\ntest\n\u00AD\u00AD\n\n\n\ntest\n \u00AD\u00AD \n\n\n\ntest\n\n\n\n\n\n\ntest',
100
- })
101
- const output = sanitizeRichText(input, { cleanNewlines: true })
102
- expect(String(output.unicodeText)).toEqual(
103
- 'test\n\ntest\n\ntest\n\ntest\n\ntest',
104
- )
105
- })
106
-
107
- it('removes more than two consecutive new lines with word joiner', () => {
108
- const input = new RichText({
109
- text: 'test\n\n\n\n\ntest\n\u2060\u2060\n\n\n\ntest\n \u2060\u2060 \n\n\n\ntest\n\n\n\n\n\n\ntest',
110
- })
111
- const output = sanitizeRichText(input, { cleanNewlines: true })
112
- expect(String(output.unicodeText)).toEqual(
113
- 'test\n\ntest\n\ntest\n\ntest\n\ntest',
114
- )
115
- })
116
- })
117
-
118
- describe('sanitizeRichText w/facets: cleanNewlines', () => {
119
- it('preserves entities as expected', () => {
120
- const input = new RichText({
121
- text: 'test\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest',
122
- facets: [
123
- { index: { byteStart: 0, byteEnd: 13 }, features: [{ $type: '' }] },
124
- { index: { byteStart: 13, byteEnd: 24 }, features: [{ $type: '' }] },
125
- { index: { byteStart: 9, byteEnd: 15 }, features: [{ $type: '' }] },
126
- { index: { byteStart: 4, byteEnd: 9 }, features: [{ $type: '' }] },
127
- ],
128
- })
129
- const output = sanitizeRichText(input, { cleanNewlines: true })
130
- expect(facetToStr(String(input.unicodeText), input.facets?.[0])).toEqual(
131
- 'test\n\n\n\n\ntest',
132
- )
133
- expect(facetToStr(String(input.unicodeText), input.facets?.[1])).toEqual(
134
- '\n\n\n\n\n',
135
- )
136
- expect(facetToStr(String(input.unicodeText), input.facets?.[2])).toEqual(
137
- 'test\n\n',
138
- )
139
- expect(facetToStr(String(input.unicodeText), input.facets?.[3])).toEqual(
140
- '\n\n\n\n\n\n\ntest',
141
- )
142
- expect(String(output.unicodeText)).toEqual(
143
- 'test\n\ntest\n\ntest\n\ntest\n\ntest',
144
- )
145
- expect(facetToStr(String(output.unicodeText), output.facets?.[0])).toEqual(
146
- 'test\n\ntest',
147
- )
148
- expect(facetToStr(String(output.unicodeText), output.facets?.[1])).toEqual(
149
- 'test',
150
- )
151
- expect(facetToStr(String(output.unicodeText), output.facets?.[2])).toEqual(
152
- 'test',
153
- )
154
- expect(output.facets?.[3]).toEqual(undefined)
155
- })
156
-
157
- it('preserves entities as expected w/fat unicode', () => {
158
- const str = new UnicodeString(
159
- 'πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\n\n\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\n\n\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\n\n\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\n\n\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\n\n\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\n\n\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\n\n\n\n',
160
- )
161
- let lastI = 0
162
- const makeFacet = (match: string) => {
163
- const i = str.utf16.indexOf(match, lastI)
164
- lastI = i + match.length
165
- const byteStart = str.utf16IndexToUtf8Index(i)
166
- const byteEnd = byteStart + new UnicodeString(match).length
167
- return {
168
- index: { byteStart, byteEnd },
169
- features: [{ $type: '' }],
170
- }
171
- }
172
-
173
- const input = new RichText({
174
- text: str.utf16,
175
- facets: [
176
- makeFacet('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\n\n\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test'),
177
- makeFacet('\n\n\n\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test'),
178
- makeFacet('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\n'),
179
- makeFacet('\n\n'),
180
- ],
181
- })
182
- const output = sanitizeRichText(input, { cleanNewlines: true })
183
- expect(facetToStr(String(input.unicodeText), input.facets?.[0])).toEqual(
184
- 'πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\n\n\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test',
185
- )
186
- expect(facetToStr(String(input.unicodeText), input.facets?.[1])).toEqual(
187
- '\n\n\n\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test',
188
- )
189
- expect(facetToStr(String(input.unicodeText), input.facets?.[2])).toEqual(
190
- 'πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\n',
191
- )
192
- expect(facetToStr(String(input.unicodeText), input.facets?.[3])).toEqual(
193
- '\n\n',
194
- )
195
- expect(String(output.unicodeText)).toEqual(
196
- 'πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\n',
197
- )
198
- expect(facetToStr(String(output.unicodeText), output.facets?.[0])).toEqual(
199
- 'πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test\n\nπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test',
200
- )
201
- expect(facetToStr(String(output.unicodeText), output.facets?.[1])).toEqual(
202
- 'πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test',
203
- )
204
- expect(facetToStr(String(output.unicodeText), output.facets?.[2])).toEqual(
205
- 'πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test',
206
- )
207
- expect(output.facets?.[3]).toEqual(undefined)
208
- })
209
- })
210
-
211
- function facetToStr(str: string, ent?: Facet) {
212
- if (!ent) {
213
- return ''
214
- }
215
- return new UnicodeString(str).slice(ent.index.byteStart, ent.index.byteEnd)
216
- }