@atproto/api 0.20.23 β†’ 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 (57) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +22 -16
  3. package/definitions/labels.json +0 -170
  4. package/docs/moderation.md +0 -260
  5. package/jest.config.cjs +0 -23
  6. package/jest.d.ts +0 -20
  7. package/jest.setup.ts +0 -97
  8. package/scripts/code/labels.mjs +0 -77
  9. package/scripts/generate-code.mjs +0 -4
  10. package/src/age-assurance.test.ts +0 -218
  11. package/src/age-assurance.ts +0 -137
  12. package/src/agent.ts +0 -1645
  13. package/src/atp-agent.ts +0 -570
  14. package/src/bsky-agent.ts +0 -14
  15. package/src/const.ts +0 -1
  16. package/src/index.ts +0 -44
  17. package/src/mocker.ts +0 -220
  18. package/src/moderation/decision.ts +0 -389
  19. package/src/moderation/index.ts +0 -69
  20. package/src/moderation/mutewords.ts +0 -170
  21. package/src/moderation/subjects/account.ts +0 -44
  22. package/src/moderation/subjects/feed-generator.ts +0 -24
  23. package/src/moderation/subjects/notification.ts +0 -25
  24. package/src/moderation/subjects/post.ts +0 -433
  25. package/src/moderation/subjects/profile.ts +0 -26
  26. package/src/moderation/subjects/status.ts +0 -27
  27. package/src/moderation/subjects/user-list.ts +0 -48
  28. package/src/moderation/types.ts +0 -191
  29. package/src/moderation/ui.ts +0 -21
  30. package/src/moderation/util.ts +0 -111
  31. package/src/predicate.ts +0 -52
  32. package/src/rich-text/detection.ts +0 -146
  33. package/src/rich-text/rich-text.ts +0 -418
  34. package/src/rich-text/sanitization.ts +0 -42
  35. package/src/rich-text/unicode.ts +0 -47
  36. package/src/rich-text/util.ts +0 -15
  37. package/src/session-manager.ts +0 -5
  38. package/src/types.ts +0 -165
  39. package/src/util.ts +0 -96
  40. package/tests/atp-agent.test.ts +0 -3863
  41. package/tests/dispatcher.test.ts +0 -527
  42. package/tests/errors.test.ts +0 -29
  43. package/tests/moderation-behaviors.test.ts +0 -951
  44. package/tests/moderation-custom-labels.test.ts +0 -334
  45. package/tests/moderation-mutewords.test.ts +0 -1299
  46. package/tests/moderation-prefs.test.ts +0 -402
  47. package/tests/moderation-quoteposts.test.ts +0 -277
  48. package/tests/moderation.test.ts +0 -739
  49. package/tests/rich-text-detection.test.ts +0 -456
  50. package/tests/rich-text-sanitization.test.ts +0 -216
  51. package/tests/rich-text.test.ts +0 -705
  52. package/tests/util/echo-server.ts +0 -37
  53. package/tests/util/moderation-behavior.ts +0 -268
  54. package/tsconfig.build.json +0 -9
  55. package/tsconfig.build.tsbuildinfo +0 -1
  56. package/tsconfig.json +0 -7
  57. package/tsconfig.tests.json +0 -10
@@ -1,705 +0,0 @@
1
- import { RichText } from '../src/index.js'
2
-
3
- describe('RichText', () => {
4
- it('converts entities to facets correctly', () => {
5
- const rt = new RichText({
6
- text: 'test',
7
- entities: [
8
- {
9
- index: { start: 0, end: 1 },
10
- type: 'link',
11
- value: 'https://example.com',
12
- },
13
- {
14
- index: { start: 1, end: 2 },
15
- type: 'mention',
16
- value: 'did:plc:1234',
17
- },
18
- {
19
- index: { start: 2, end: 3 },
20
- type: 'other',
21
- value: 'willbedropped',
22
- },
23
- ],
24
- })
25
- expect(rt.facets).toEqual([
26
- {
27
- $type: 'app.bsky.richtext.facet',
28
- index: { byteStart: 0, byteEnd: 1 },
29
- features: [
30
- {
31
- $type: 'app.bsky.richtext.facet#link',
32
- uri: 'https://example.com',
33
- },
34
- ],
35
- },
36
- {
37
- $type: 'app.bsky.richtext.facet',
38
- index: { byteStart: 1, byteEnd: 2 },
39
- features: [
40
- {
41
- $type: 'app.bsky.richtext.facet#mention',
42
- did: 'did:plc:1234',
43
- },
44
- ],
45
- },
46
- ])
47
- })
48
-
49
- it('converts entity utf16 indices to facet utf8 indices', () => {
50
- const rt = new RichText({
51
- text: 'πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§',
52
- entities: [
53
- {
54
- index: { start: 0, end: 11 },
55
- type: 'link',
56
- value: 'https://example.com',
57
- },
58
- {
59
- index: { start: 11, end: 22 },
60
- type: 'mention',
61
- value: 'did:plc:1234',
62
- },
63
- {
64
- index: { start: 22, end: 33 },
65
- type: 'other',
66
- value: 'willbedropped',
67
- },
68
- ],
69
- })
70
- expect(rt.facets).toEqual([
71
- {
72
- $type: 'app.bsky.richtext.facet',
73
- index: { byteStart: 0, byteEnd: 25 },
74
- features: [
75
- {
76
- $type: 'app.bsky.richtext.facet#link',
77
- uri: 'https://example.com',
78
- },
79
- ],
80
- },
81
- {
82
- $type: 'app.bsky.richtext.facet',
83
- index: { byteStart: 25, byteEnd: 50 },
84
- features: [
85
- {
86
- $type: 'app.bsky.richtext.facet#mention',
87
- did: 'did:plc:1234',
88
- },
89
- ],
90
- },
91
- ])
92
- })
93
-
94
- it('calculates bytelength and grapheme length correctly', () => {
95
- {
96
- const rt = new RichText({ text: 'Hello!' })
97
- expect(rt.length).toBe(6)
98
- expect(rt.graphemeLength).toBe(6)
99
- }
100
- {
101
- const rt = new RichText({ text: 'πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§' })
102
- expect(rt.length).toBe(25)
103
- expect(rt.graphemeLength).toBe(1)
104
- }
105
- {
106
- const rt = new RichText({ text: 'πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§πŸ”₯ good!βœ…' })
107
- expect(rt.length).toBe(38)
108
- expect(rt.graphemeLength).toBe(9)
109
- }
110
- })
111
- })
112
-
113
- describe('RichText#insert', () => {
114
- const input = new RichText({
115
- text: 'hello world',
116
- facets: [
117
- { index: { byteStart: 2, byteEnd: 7 }, features: [{ $type: '' }] },
118
- ],
119
- })
120
-
121
- it('correctly adjusts facets (scenario A - before)', () => {
122
- const output = input.clone().insert(0, 'test')
123
- expect(output.text).toEqual('testhello world')
124
- expect(output.facets?.[0].index.byteStart).toEqual(6)
125
- expect(output.facets?.[0].index.byteEnd).toEqual(11)
126
- expect(
127
- output.unicodeText.slice(
128
- output.facets?.[0].index.byteStart,
129
- output.facets?.[0].index.byteEnd,
130
- ),
131
- ).toEqual('llo w')
132
- })
133
-
134
- it('correctly adjusts facets (scenario B - inner)', () => {
135
- const output = input.clone().insert(4, 'test')
136
- expect(output.text).toEqual('helltesto world')
137
- expect(output.facets?.[0].index.byteStart).toEqual(2)
138
- expect(output.facets?.[0].index.byteEnd).toEqual(11)
139
- expect(
140
- output.unicodeText.slice(
141
- output.facets?.[0].index.byteStart,
142
- output.facets?.[0].index.byteEnd,
143
- ),
144
- ).toEqual('lltesto w')
145
- })
146
-
147
- it('correctly adjusts facets (scenario C - after)', () => {
148
- const output = input.clone().insert(8, 'test')
149
- expect(output.text).toEqual('hello wotestrld')
150
- expect(output.facets?.[0].index.byteStart).toEqual(2)
151
- expect(output.facets?.[0].index.byteEnd).toEqual(7)
152
- expect(
153
- output.unicodeText.slice(
154
- output.facets?.[0].index.byteStart,
155
- output.facets?.[0].index.byteEnd,
156
- ),
157
- ).toEqual('llo w')
158
- })
159
- })
160
-
161
- describe('RichText#insert w/fat unicode', () => {
162
- const input = new RichText({
163
- text: 'oneπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§',
164
- facets: [
165
- { index: { byteStart: 0, byteEnd: 28 }, features: [{ $type: '' }] },
166
- { index: { byteStart: 29, byteEnd: 57 }, features: [{ $type: '' }] },
167
- { index: { byteStart: 58, byteEnd: 88 }, features: [{ $type: '' }] },
168
- ],
169
- })
170
-
171
- it('correctly adjusts facets (scenario A - before)', () => {
172
- const output = input.clone().insert(0, 'test')
173
- expect(output.text).toEqual('testoneπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
174
- expect(
175
- output.unicodeText.slice(
176
- output.facets?.[0].index.byteStart,
177
- output.facets?.[0].index.byteEnd,
178
- ),
179
- ).toEqual('oneπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
180
- expect(
181
- output.unicodeText.slice(
182
- output.facets?.[1].index.byteStart,
183
- output.facets?.[1].index.byteEnd,
184
- ),
185
- ).toEqual('twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
186
- expect(
187
- output.unicodeText.slice(
188
- output.facets?.[2].index.byteStart,
189
- output.facets?.[2].index.byteEnd,
190
- ),
191
- ).toEqual('threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
192
- })
193
-
194
- it('correctly adjusts facets (scenario B - inner)', () => {
195
- const output = input.clone().insert(3, 'test')
196
- expect(output.text).toEqual('onetestπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
197
- expect(
198
- output.unicodeText.slice(
199
- output.facets?.[0].index.byteStart,
200
- output.facets?.[0].index.byteEnd,
201
- ),
202
- ).toEqual('onetestπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
203
- expect(
204
- output.unicodeText.slice(
205
- output.facets?.[1].index.byteStart,
206
- output.facets?.[1].index.byteEnd,
207
- ),
208
- ).toEqual('twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
209
- expect(
210
- output.unicodeText.slice(
211
- output.facets?.[2].index.byteStart,
212
- output.facets?.[2].index.byteEnd,
213
- ),
214
- ).toEqual('threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
215
- })
216
-
217
- it('correctly adjusts facets (scenario C - after)', () => {
218
- const output = input.clone().insert(28, 'test')
219
- expect(output.text).toEqual('oneπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§test twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
220
- expect(
221
- output.unicodeText.slice(
222
- output.facets?.[0].index.byteStart,
223
- output.facets?.[0].index.byteEnd,
224
- ),
225
- ).toEqual('oneπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
226
- expect(
227
- output.unicodeText.slice(
228
- output.facets?.[1].index.byteStart,
229
- output.facets?.[1].index.byteEnd,
230
- ),
231
- ).toEqual('twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
232
- expect(
233
- output.unicodeText.slice(
234
- output.facets?.[2].index.byteStart,
235
- output.facets?.[2].index.byteEnd,
236
- ),
237
- ).toEqual('threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
238
- })
239
- })
240
-
241
- describe('RichText#delete', () => {
242
- const input = new RichText({
243
- text: 'hello world',
244
- facets: [
245
- { index: { byteStart: 2, byteEnd: 7 }, features: [{ $type: '' }] },
246
- ],
247
- })
248
-
249
- it('correctly adjusts facets (scenario A - entirely outer)', () => {
250
- const output = input.clone().delete(0, 9)
251
- expect(output.text).toEqual('ld')
252
- expect(output.facets?.length).toEqual(0)
253
- })
254
-
255
- it('correctly adjusts facets (scenario B - entirely after)', () => {
256
- const output = input.clone().delete(7, 11)
257
- expect(output.text).toEqual('hello w')
258
- expect(output.facets?.[0].index.byteStart).toEqual(2)
259
- expect(output.facets?.[0].index.byteEnd).toEqual(7)
260
- expect(
261
- output.unicodeText.slice(
262
- output.facets?.[0].index.byteStart,
263
- output.facets?.[0].index.byteEnd,
264
- ),
265
- ).toEqual('llo w')
266
- })
267
-
268
- it('correctly adjusts facets (scenario C - partially after)', () => {
269
- const output = input.clone().delete(4, 11)
270
- expect(output.text).toEqual('hell')
271
- expect(output.facets?.[0].index.byteStart).toEqual(2)
272
- expect(output.facets?.[0].index.byteEnd).toEqual(4)
273
- expect(
274
- output.unicodeText.slice(
275
- output.facets?.[0].index.byteStart,
276
- output.facets?.[0].index.byteEnd,
277
- ),
278
- ).toEqual('ll')
279
- })
280
-
281
- it('correctly adjusts facets (scenario D - entirely inner)', () => {
282
- const output = input.clone().delete(3, 5)
283
- expect(output.text).toEqual('hel world')
284
- expect(output.facets?.[0].index.byteStart).toEqual(2)
285
- expect(output.facets?.[0].index.byteEnd).toEqual(5)
286
- expect(
287
- output.unicodeText.slice(
288
- output.facets?.[0].index.byteStart,
289
- output.facets?.[0].index.byteEnd,
290
- ),
291
- ).toEqual('l w')
292
- })
293
-
294
- it('correctly adjusts facets (scenario E - partially before)', () => {
295
- const output = input.clone().delete(1, 5)
296
- expect(output.text).toEqual('h world')
297
- expect(output.facets?.[0].index.byteStart).toEqual(1)
298
- expect(output.facets?.[0].index.byteEnd).toEqual(3)
299
- expect(
300
- output.unicodeText.slice(
301
- output.facets?.[0].index.byteStart,
302
- output.facets?.[0].index.byteEnd,
303
- ),
304
- ).toEqual(' w')
305
- })
306
-
307
- it('correctly adjusts facets (scenario F - entirely before)', () => {
308
- const output = input.clone().delete(0, 2)
309
- expect(output.text).toEqual('llo world')
310
- expect(output.facets?.[0].index.byteStart).toEqual(0)
311
- expect(output.facets?.[0].index.byteEnd).toEqual(5)
312
- expect(
313
- output.unicodeText.slice(
314
- output.facets?.[0].index.byteStart,
315
- output.facets?.[0].index.byteEnd,
316
- ),
317
- ).toEqual('llo w')
318
- })
319
- })
320
-
321
- describe('RichText#delete w/fat unicode', () => {
322
- const input = new RichText({
323
- text: 'oneπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§',
324
- facets: [
325
- { index: { byteStart: 29, byteEnd: 57 }, features: [{ $type: '' }] },
326
- ],
327
- })
328
-
329
- it('correctly adjusts facets (scenario A - entirely outer)', () => {
330
- const output = input.clone().delete(28, 58)
331
- expect(output.text).toEqual('oneπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
332
- expect(output.facets?.length).toEqual(0)
333
- })
334
-
335
- it('correctly adjusts facets (scenario B - entirely after)', () => {
336
- const output = input.clone().delete(57, 88)
337
- expect(output.text).toEqual('oneπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
338
- expect(output.facets?.[0].index.byteStart).toEqual(29)
339
- expect(output.facets?.[0].index.byteEnd).toEqual(57)
340
- expect(
341
- output.unicodeText.slice(
342
- output.facets?.[0].index.byteStart,
343
- output.facets?.[0].index.byteEnd,
344
- ),
345
- ).toEqual('twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
346
- })
347
-
348
- it('correctly adjusts facets (scenario C - partially after)', () => {
349
- const output = input.clone().delete(31, 88)
350
- expect(output.text).toEqual('oneπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ tw')
351
- expect(output.facets?.[0].index.byteStart).toEqual(29)
352
- expect(output.facets?.[0].index.byteEnd).toEqual(31)
353
- expect(
354
- output.unicodeText.slice(
355
- output.facets?.[0].index.byteStart,
356
- output.facets?.[0].index.byteEnd,
357
- ),
358
- ).toEqual('tw')
359
- })
360
-
361
- it('correctly adjusts facets (scenario D - entirely inner)', () => {
362
- const output = input.clone().delete(30, 32)
363
- expect(output.text).toEqual('oneπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ tπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
364
- expect(output.facets?.[0].index.byteStart).toEqual(29)
365
- expect(output.facets?.[0].index.byteEnd).toEqual(55)
366
- expect(
367
- output.unicodeText.slice(
368
- output.facets?.[0].index.byteStart,
369
- output.facets?.[0].index.byteEnd,
370
- ),
371
- ).toEqual('tπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
372
- })
373
-
374
- it('correctly adjusts facets (scenario E - partially before)', () => {
375
- const output = input.clone().delete(28, 31)
376
- expect(output.text).toEqual('oneπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§oπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
377
- expect(output.facets?.[0].index.byteStart).toEqual(28)
378
- expect(output.facets?.[0].index.byteEnd).toEqual(54)
379
- expect(
380
- output.unicodeText.slice(
381
- output.facets?.[0].index.byteStart,
382
- output.facets?.[0].index.byteEnd,
383
- ),
384
- ).toEqual('oπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
385
- })
386
-
387
- it('correctly adjusts facets (scenario F - entirely before)', () => {
388
- const output = input.clone().delete(0, 2)
389
- expect(output.text).toEqual('eπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
390
- expect(output.facets?.[0].index.byteStart).toEqual(27)
391
- expect(output.facets?.[0].index.byteEnd).toEqual(55)
392
- expect(
393
- output.unicodeText.slice(
394
- output.facets?.[0].index.byteStart,
395
- output.facets?.[0].index.byteEnd,
396
- ),
397
- ).toEqual('twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§')
398
- })
399
- })
400
-
401
- describe('RichText#segments', () => {
402
- it('produces an empty output for an empty input', () => {
403
- const input = new RichText({ text: '' })
404
- expect(Array.from(input.segments())).toEqual([{ text: '' }])
405
- })
406
-
407
- it('produces a single segment when no facets are present', () => {
408
- const input = new RichText({ text: 'hello' })
409
- expect(Array.from(input.segments())).toEqual([{ text: 'hello' }])
410
- })
411
-
412
- it('produces 3 segments with 1 entity in the middle', () => {
413
- const input = new RichText({
414
- text: 'one two three',
415
- facets: [
416
- { index: { byteStart: 4, byteEnd: 7 }, features: [{ $type: '' }] },
417
- ],
418
- })
419
- expect(Array.from(input.segments())).toEqual([
420
- { text: 'one ' },
421
- {
422
- text: 'two',
423
- facet: {
424
- index: { byteStart: 4, byteEnd: 7 },
425
- features: [{ $type: '' }],
426
- },
427
- },
428
- { text: ' three' },
429
- ])
430
- })
431
-
432
- it('produces 2 segments with 1 entity in the byteStart', () => {
433
- const input = new RichText({
434
- text: 'one two three',
435
- facets: [
436
- { index: { byteStart: 0, byteEnd: 7 }, features: [{ $type: '' }] },
437
- ],
438
- })
439
- expect(Array.from(input.segments())).toEqual([
440
- {
441
- text: 'one two',
442
- facet: {
443
- index: { byteStart: 0, byteEnd: 7 },
444
- features: [{ $type: '' }],
445
- },
446
- },
447
- { text: ' three' },
448
- ])
449
- })
450
-
451
- it('produces 2 segments with 1 entity in the end', () => {
452
- const input = new RichText({
453
- text: 'one two three',
454
- facets: [
455
- { index: { byteStart: 4, byteEnd: 13 }, features: [{ $type: '' }] },
456
- ],
457
- })
458
- expect(Array.from(input.segments())).toEqual([
459
- { text: 'one ' },
460
- {
461
- text: 'two three',
462
- facet: {
463
- index: { byteStart: 4, byteEnd: 13 },
464
- features: [{ $type: '' }],
465
- },
466
- },
467
- ])
468
- })
469
-
470
- it('produces 1 segments with 1 entity around the entire string', () => {
471
- const input = new RichText({
472
- text: 'one two three',
473
- facets: [
474
- { index: { byteStart: 0, byteEnd: 13 }, features: [{ $type: '' }] },
475
- ],
476
- })
477
- expect(Array.from(input.segments())).toEqual([
478
- {
479
- text: 'one two three',
480
- facet: {
481
- index: { byteStart: 0, byteEnd: 13 },
482
- features: [{ $type: '' }],
483
- },
484
- },
485
- ])
486
- })
487
-
488
- it('produces 5 segments with 3 facets covering each word', () => {
489
- const input = new RichText({
490
- text: 'one two three',
491
- facets: [
492
- { index: { byteStart: 0, byteEnd: 3 }, features: [{ $type: '' }] },
493
- { index: { byteStart: 4, byteEnd: 7 }, features: [{ $type: '' }] },
494
- { index: { byteStart: 8, byteEnd: 13 }, features: [{ $type: '' }] },
495
- ],
496
- })
497
- expect(Array.from(input.segments())).toEqual([
498
- {
499
- text: 'one',
500
- facet: {
501
- index: { byteStart: 0, byteEnd: 3 },
502
- features: [{ $type: '' }],
503
- },
504
- },
505
- { text: ' ' },
506
- {
507
- text: 'two',
508
- facet: {
509
- index: { byteStart: 4, byteEnd: 7 },
510
- features: [{ $type: '' }],
511
- },
512
- },
513
- { text: ' ' },
514
- {
515
- text: 'three',
516
- facet: {
517
- index: { byteStart: 8, byteEnd: 13 },
518
- features: [{ $type: '' }],
519
- },
520
- },
521
- ])
522
- })
523
-
524
- it('uses utf8 indices', () => {
525
- const input = new RichText({
526
- text: 'oneπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§ threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§',
527
- facets: [
528
- { index: { byteStart: 0, byteEnd: 28 }, features: [{ $type: '' }] },
529
- { index: { byteStart: 29, byteEnd: 57 }, features: [{ $type: '' }] },
530
- { index: { byteStart: 58, byteEnd: 88 }, features: [{ $type: '' }] },
531
- ],
532
- })
533
- expect(Array.from(input.segments())).toEqual([
534
- {
535
- text: 'oneπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§',
536
- facet: {
537
- index: { byteStart: 0, byteEnd: 28 },
538
- features: [{ $type: '' }],
539
- },
540
- },
541
- { text: ' ' },
542
- {
543
- text: 'twoπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§',
544
- facet: {
545
- index: { byteStart: 29, byteEnd: 57 },
546
- features: [{ $type: '' }],
547
- },
548
- },
549
- { text: ' ' },
550
- {
551
- text: 'threeπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§',
552
- facet: {
553
- index: { byteStart: 58, byteEnd: 88 },
554
- features: [{ $type: '' }],
555
- },
556
- },
557
- ])
558
- })
559
-
560
- it('correctly identifies mentions and links', () => {
561
- const input = new RichText({
562
- text: 'one two three',
563
- facets: [
564
- {
565
- index: { byteStart: 0, byteEnd: 3 },
566
- features: [
567
- {
568
- $type: 'app.bsky.richtext.facet#mention',
569
- did: 'did:plc:123',
570
- },
571
- ],
572
- },
573
- {
574
- index: { byteStart: 4, byteEnd: 7 },
575
- features: [
576
- {
577
- $type: 'app.bsky.richtext.facet#link',
578
- uri: 'https://example.com',
579
- },
580
- ],
581
- },
582
- {
583
- index: { byteStart: 8, byteEnd: 13 },
584
- features: [{ $type: 'other' }],
585
- },
586
- ],
587
- })
588
- const segments = Array.from(input.segments())
589
- expect(segments[0].isLink()).toBe(false)
590
- expect(segments[0].isMention()).toBe(true)
591
- expect(segments[1].isLink()).toBe(false)
592
- expect(segments[1].isMention()).toBe(false)
593
- expect(segments[2].isLink()).toBe(true)
594
- expect(segments[2].isMention()).toBe(false)
595
- expect(segments[3].isLink()).toBe(false)
596
- expect(segments[3].isMention()).toBe(false)
597
- expect(segments[4].isLink()).toBe(false)
598
- expect(segments[4].isMention()).toBe(false)
599
- })
600
-
601
- it('skips facets that incorrectly overlap (left edge)', () => {
602
- const input = new RichText({
603
- text: 'one two three',
604
- facets: [
605
- { index: { byteStart: 0, byteEnd: 3 }, features: [{ $type: '' }] },
606
- { index: { byteStart: 2, byteEnd: 9 }, features: [{ $type: '' }] },
607
- { index: { byteStart: 8, byteEnd: 13 }, features: [{ $type: '' }] },
608
- ],
609
- })
610
- expect(Array.from(input.segments())).toEqual([
611
- {
612
- text: 'one',
613
- facet: {
614
- index: { byteStart: 0, byteEnd: 3 },
615
- features: [{ $type: '' }],
616
- },
617
- },
618
- {
619
- text: ' two ',
620
- },
621
- {
622
- text: 'three',
623
- facet: {
624
- index: { byteStart: 8, byteEnd: 13 },
625
- features: [{ $type: '' }],
626
- },
627
- },
628
- ])
629
- })
630
-
631
- it('skips facets that incorrectly overlap (right edge)', () => {
632
- const input = new RichText({
633
- text: 'one two three',
634
- facets: [
635
- { index: { byteStart: 0, byteEnd: 3 }, features: [{ $type: '' }] },
636
- { index: { byteStart: 4, byteEnd: 9 }, features: [{ $type: '' }] },
637
- { index: { byteStart: 8, byteEnd: 13 }, features: [{ $type: '' }] },
638
- ],
639
- })
640
- expect(Array.from(input.segments())).toEqual([
641
- {
642
- text: 'one',
643
- facet: {
644
- index: { byteStart: 0, byteEnd: 3 },
645
- features: [{ $type: '' }],
646
- },
647
- },
648
- { text: ' ' },
649
- {
650
- text: 'two t',
651
- facet: {
652
- index: { byteStart: 4, byteEnd: 9 },
653
- features: [{ $type: '' }],
654
- },
655
- },
656
- {
657
- text: 'hree',
658
- },
659
- ])
660
- })
661
-
662
- it("doesn't duplicate text when negative-length facets are present", () => {
663
- const input = {
664
- text: 'hello world',
665
- facets: [
666
- // invalid zero-length
667
- {
668
- features: [],
669
- index: {
670
- byteStart: 6,
671
- byteEnd: 0,
672
- },
673
- },
674
- // valid normal facet
675
- {
676
- features: [],
677
- index: {
678
- byteEnd: 11,
679
- byteStart: 6,
680
- },
681
- },
682
- // valid zero-length
683
- {
684
- features: [],
685
- index: {
686
- byteEnd: 0,
687
- byteStart: 0,
688
- },
689
- },
690
- ],
691
- }
692
-
693
- const rt = new RichText(input)
694
-
695
- let output = ''
696
- for (const segment of rt.segments()) {
697
- output += segment.text
698
- }
699
-
700
- expect(output).toEqual(input.text)
701
-
702
- // invalid one should have been removed
703
- expect(rt.facets?.length).toEqual(2)
704
- })
705
- })