this repo has no description
1import { RichText } from '../src'
2
3describe('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
113describe('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
161describe('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
241describe('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
321describe('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
401describe('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})