@atproto/api 0.18.13 → 0.18.15

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 (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/agent.d.ts +9 -0
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +35 -0
  5. package/dist/agent.js.map +1 -1
  6. package/dist/client/lexicons.d.ts +38 -2
  7. package/dist/client/lexicons.d.ts.map +1 -1
  8. package/dist/client/lexicons.js +19 -0
  9. package/dist/client/lexicons.js.map +1 -1
  10. package/dist/client/types/app/bsky/actor/defs.d.ts +11 -1
  11. package/dist/client/types/app/bsky/actor/defs.d.ts.map +1 -1
  12. package/dist/client/types/app/bsky/actor/defs.js +9 -0
  13. package/dist/client/types/app/bsky/actor/defs.js.map +1 -1
  14. package/dist/predicate.d.ts +1 -0
  15. package/dist/predicate.d.ts.map +1 -1
  16. package/dist/predicate.js +2 -1
  17. package/dist/predicate.js.map +1 -1
  18. package/dist/rich-text/detection.d.ts.map +1 -1
  19. package/dist/rich-text/detection.js +25 -0
  20. package/dist/rich-text/detection.js.map +1 -1
  21. package/dist/rich-text/util.d.ts +1 -0
  22. package/dist/rich-text/util.d.ts.map +1 -1
  23. package/dist/rich-text/util.js +2 -1
  24. package/dist/rich-text/util.js.map +1 -1
  25. package/dist/types.d.ts +4 -0
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/types.js.map +1 -1
  28. package/package.json +2 -2
  29. package/src/agent.ts +44 -0
  30. package/src/client/lexicons.ts +20 -0
  31. package/src/client/types/app/bsky/actor/defs.ts +20 -0
  32. package/src/predicate.ts +3 -0
  33. package/src/rich-text/detection.ts +29 -0
  34. package/src/rich-text/util.ts +3 -0
  35. package/src/types.ts +4 -0
  36. package/tests/atp-agent.test.ts +180 -0
  37. package/tests/moderation-prefs.test.ts +16 -0
  38. package/tests/rich-text-detection.test.ts +68 -0
@@ -269,6 +269,7 @@ export type Preferences = (
269
269
  | $Typed<LabelersPref>
270
270
  | $Typed<PostInteractionSettingsPref>
271
271
  | $Typed<VerificationPrefs>
272
+ | $Typed<LiveEventPreferences>
272
273
  | { $type: string }
273
274
  )[]
274
275
 
@@ -618,6 +619,25 @@ export function validateVerificationPrefs<V>(v: V) {
618
619
  return validate<VerificationPrefs & V>(v, id, hashVerificationPrefs)
619
620
  }
620
621
 
622
+ /** Preferences for live events. */
623
+ export interface LiveEventPreferences {
624
+ $type?: 'app.bsky.actor.defs#liveEventPreferences'
625
+ /** A list of feed IDs that the user has hidden from live events. */
626
+ hiddenFeedIds?: string[]
627
+ /** Whether to hide all feeds from live events. */
628
+ hideAllFeeds: boolean
629
+ }
630
+
631
+ const hashLiveEventPreferences = 'liveEventPreferences'
632
+
633
+ export function isLiveEventPreferences<V>(v: V) {
634
+ return is$typed(v, id, hashLiveEventPreferences)
635
+ }
636
+
637
+ export function validateLiveEventPreferences<V>(v: V) {
638
+ return validate<LiveEventPreferences & V>(v, id, hashLiveEventPreferences)
639
+ }
640
+
621
641
  /** Default post interaction settings for the account. These values should be applied as default values when creating new posts. These refs should mirror the threadgate and postgate records exactly. */
622
642
  export interface PostInteractionSettingsPref {
623
643
  $type?: 'app.bsky.actor.defs#postInteractionSettingsPref'
package/src/predicate.ts CHANGED
@@ -47,3 +47,6 @@ export const isValidThreadViewPref = asPredicate(
47
47
  export const isValidVerificationPrefs = asPredicate(
48
48
  AppBskyActorDefs.validateVerificationPrefs,
49
49
  )
50
+ export const isValidLiveEventPreferences = asPredicate(
51
+ AppBskyActorDefs.validateLiveEventPreferences,
52
+ )
@@ -2,6 +2,7 @@ import TLDs from 'tlds'
2
2
  import { AppBskyRichtextFacet } from '../client'
3
3
  import { UnicodeString } from './unicode'
4
4
  import {
5
+ CASHTAG_REGEX,
5
6
  MENTION_REGEX,
6
7
  TAG_REGEX,
7
8
  TRAILING_PUNCTUATION_REGEX,
@@ -103,6 +104,34 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined {
103
104
  })
104
105
  }
105
106
  }
107
+ {
108
+ // cashtags
109
+ const re = CASHTAG_REGEX
110
+ while ((match = re.exec(text.utf16))) {
111
+ const leading = match[1]
112
+ let ticker = match[2]
113
+
114
+ if (!ticker) continue
115
+
116
+ // Normalize to uppercase
117
+ ticker = ticker.toUpperCase()
118
+
119
+ const index = match.index + leading.length
120
+
121
+ facets.push({
122
+ index: {
123
+ byteStart: text.utf16IndexToUtf8Index(index),
124
+ byteEnd: text.utf16IndexToUtf8Index(index + 1 + ticker.length), // +1 for $
125
+ },
126
+ features: [
127
+ {
128
+ $type: 'app.bsky.richtext.facet#tag',
129
+ tag: '$' + ticker, // Store with $ prefix
130
+ },
131
+ ],
132
+ })
133
+ }
134
+ }
106
135
  return facets.length > 0 ? facets : undefined
107
136
  }
108
137
 
@@ -10,3 +10,6 @@ export const TRAILING_PUNCTUATION_REGEX = /\p{P}+$/gu
10
10
  export const TAG_REGEX =
11
11
  // eslint-disable-next-line no-misleading-character-class
12
12
  /(^|\s)[##]((?!\ufe0f)[^\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]*[^\d\s\p{P}\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]+[^\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]*)?/gu
13
+
14
+ export const CASHTAG_REGEX =
15
+ /(^|\s|\()\$([A-Za-z][A-Za-z0-9]{0,4})(?=\s|$|[.,;:!?)"'\u2019])/gu
package/src/types.ts CHANGED
@@ -158,4 +158,8 @@ export interface BskyPreferences {
158
158
  }
159
159
  postInteractionSettings: AppBskyActorDefs.PostInteractionSettingsPref
160
160
  verificationPrefs: AppBskyActorDefs.VerificationPrefs
161
+ liveEventPreferences: {
162
+ hiddenFeedIds: string[]
163
+ hideAllFeeds: boolean
164
+ }
161
165
  }
@@ -285,6 +285,10 @@ describe('agent', () => {
285
285
  verificationPrefs: {
286
286
  hideBadges: false,
287
287
  },
288
+ liveEventPreferences: {
289
+ hiddenFeedIds: [],
290
+ hideAllFeeds: false,
291
+ },
288
292
  })
289
293
 
290
294
  await agent.setAdultContentEnabled(true)
@@ -333,6 +337,10 @@ describe('agent', () => {
333
337
  verificationPrefs: {
334
338
  hideBadges: false,
335
339
  },
340
+ liveEventPreferences: {
341
+ hiddenFeedIds: [],
342
+ hideAllFeeds: false,
343
+ },
336
344
  })
337
345
 
338
346
  await agent.setAdultContentEnabled(false)
@@ -381,6 +389,10 @@ describe('agent', () => {
381
389
  verificationPrefs: {
382
390
  hideBadges: false,
383
391
  },
392
+ liveEventPreferences: {
393
+ hiddenFeedIds: [],
394
+ hideAllFeeds: false,
395
+ },
384
396
  })
385
397
 
386
398
  await agent.setContentLabelPref('misinfo', 'hide')
@@ -429,6 +441,10 @@ describe('agent', () => {
429
441
  verificationPrefs: {
430
442
  hideBadges: false,
431
443
  },
444
+ liveEventPreferences: {
445
+ hiddenFeedIds: [],
446
+ hideAllFeeds: false,
447
+ },
432
448
  })
433
449
 
434
450
  await agent.setContentLabelPref('spam', 'ignore')
@@ -481,6 +497,10 @@ describe('agent', () => {
481
497
  verificationPrefs: {
482
498
  hideBadges: false,
483
499
  },
500
+ liveEventPreferences: {
501
+ hiddenFeedIds: [],
502
+ hideAllFeeds: false,
503
+ },
484
504
  })
485
505
 
486
506
  await agent.addSavedFeed('at://bob.com/app.bsky.feed.generator/fake')
@@ -536,6 +556,10 @@ describe('agent', () => {
536
556
  verificationPrefs: {
537
557
  hideBadges: false,
538
558
  },
559
+ liveEventPreferences: {
560
+ hiddenFeedIds: [],
561
+ hideAllFeeds: false,
562
+ },
539
563
  })
540
564
 
541
565
  await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
@@ -591,6 +615,10 @@ describe('agent', () => {
591
615
  verificationPrefs: {
592
616
  hideBadges: false,
593
617
  },
618
+ liveEventPreferences: {
619
+ hiddenFeedIds: [],
620
+ hideAllFeeds: false,
621
+ },
594
622
  })
595
623
 
596
624
  await agent.removePinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
@@ -646,6 +674,10 @@ describe('agent', () => {
646
674
  verificationPrefs: {
647
675
  hideBadges: false,
648
676
  },
677
+ liveEventPreferences: {
678
+ hiddenFeedIds: [],
679
+ hideAllFeeds: false,
680
+ },
649
681
  })
650
682
 
651
683
  await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake')
@@ -701,6 +733,10 @@ describe('agent', () => {
701
733
  verificationPrefs: {
702
734
  hideBadges: false,
703
735
  },
736
+ liveEventPreferences: {
737
+ hiddenFeedIds: [],
738
+ hideAllFeeds: false,
739
+ },
704
740
  })
705
741
 
706
742
  await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
@@ -756,6 +792,10 @@ describe('agent', () => {
756
792
  verificationPrefs: {
757
793
  hideBadges: false,
758
794
  },
795
+ liveEventPreferences: {
796
+ hiddenFeedIds: [],
797
+ hideAllFeeds: false,
798
+ },
759
799
  })
760
800
 
761
801
  await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake2')
@@ -817,6 +857,10 @@ describe('agent', () => {
817
857
  verificationPrefs: {
818
858
  hideBadges: false,
819
859
  },
860
+ liveEventPreferences: {
861
+ hiddenFeedIds: [],
862
+ hideAllFeeds: false,
863
+ },
820
864
  })
821
865
 
822
866
  await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake')
@@ -872,6 +916,10 @@ describe('agent', () => {
872
916
  verificationPrefs: {
873
917
  hideBadges: false,
874
918
  },
919
+ liveEventPreferences: {
920
+ hiddenFeedIds: [],
921
+ hideAllFeeds: false,
922
+ },
875
923
  })
876
924
 
877
925
  await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' })
@@ -932,6 +980,10 @@ describe('agent', () => {
932
980
  verificationPrefs: {
933
981
  hideBadges: false,
934
982
  },
983
+ liveEventPreferences: {
984
+ hiddenFeedIds: [],
985
+ hideAllFeeds: false,
986
+ },
935
987
  })
936
988
 
937
989
  await agent.setFeedViewPrefs('home', { hideReplies: true })
@@ -992,6 +1044,10 @@ describe('agent', () => {
992
1044
  verificationPrefs: {
993
1045
  hideBadges: false,
994
1046
  },
1047
+ liveEventPreferences: {
1048
+ hiddenFeedIds: [],
1049
+ hideAllFeeds: false,
1050
+ },
995
1051
  })
996
1052
 
997
1053
  await agent.setFeedViewPrefs('home', { hideReplies: false })
@@ -1052,6 +1108,10 @@ describe('agent', () => {
1052
1108
  verificationPrefs: {
1053
1109
  hideBadges: false,
1054
1110
  },
1111
+ liveEventPreferences: {
1112
+ hiddenFeedIds: [],
1113
+ hideAllFeeds: false,
1114
+ },
1055
1115
  })
1056
1116
 
1057
1117
  await agent.setFeedViewPrefs('other', { hideReplies: true })
@@ -1119,6 +1179,10 @@ describe('agent', () => {
1119
1179
  verificationPrefs: {
1120
1180
  hideBadges: false,
1121
1181
  },
1182
+ liveEventPreferences: {
1183
+ hiddenFeedIds: [],
1184
+ hideAllFeeds: false,
1185
+ },
1122
1186
  })
1123
1187
 
1124
1188
  await agent.setThreadViewPrefs({ sort: 'random' })
@@ -1186,6 +1250,10 @@ describe('agent', () => {
1186
1250
  verificationPrefs: {
1187
1251
  hideBadges: false,
1188
1252
  },
1253
+ liveEventPreferences: {
1254
+ hiddenFeedIds: [],
1255
+ hideAllFeeds: false,
1256
+ },
1189
1257
  })
1190
1258
 
1191
1259
  await agent.setThreadViewPrefs({ sort: 'oldest' })
@@ -1253,6 +1321,10 @@ describe('agent', () => {
1253
1321
  verificationPrefs: {
1254
1322
  hideBadges: false,
1255
1323
  },
1324
+ liveEventPreferences: {
1325
+ hiddenFeedIds: [],
1326
+ hideAllFeeds: false,
1327
+ },
1256
1328
  })
1257
1329
 
1258
1330
  await agent.setInterestsPref({ tags: ['foo', 'bar'] })
@@ -1320,6 +1392,10 @@ describe('agent', () => {
1320
1392
  verificationPrefs: {
1321
1393
  hideBadges: false,
1322
1394
  },
1395
+ liveEventPreferences: {
1396
+ hiddenFeedIds: [],
1397
+ hideAllFeeds: false,
1398
+ },
1323
1399
  })
1324
1400
  })
1325
1401
 
@@ -1518,6 +1594,10 @@ describe('agent', () => {
1518
1594
  verificationPrefs: {
1519
1595
  hideBadges: false,
1520
1596
  },
1597
+ liveEventPreferences: {
1598
+ hiddenFeedIds: [],
1599
+ hideAllFeeds: false,
1600
+ },
1521
1601
  })
1522
1602
 
1523
1603
  await agent.setAdultContentEnabled(false)
@@ -1587,6 +1667,10 @@ describe('agent', () => {
1587
1667
  verificationPrefs: {
1588
1668
  hideBadges: false,
1589
1669
  },
1670
+ liveEventPreferences: {
1671
+ hiddenFeedIds: [],
1672
+ hideAllFeeds: false,
1673
+ },
1590
1674
  })
1591
1675
 
1592
1676
  await agent.setContentLabelPref('porn', 'ignore')
@@ -1657,6 +1741,10 @@ describe('agent', () => {
1657
1741
  verificationPrefs: {
1658
1742
  hideBadges: false,
1659
1743
  },
1744
+ liveEventPreferences: {
1745
+ hiddenFeedIds: [],
1746
+ hideAllFeeds: false,
1747
+ },
1660
1748
  })
1661
1749
 
1662
1750
  await agent.removeLabeler('did:plc:other')
@@ -1723,6 +1811,10 @@ describe('agent', () => {
1723
1811
  verificationPrefs: {
1724
1812
  hideBadges: false,
1725
1813
  },
1814
+ liveEventPreferences: {
1815
+ hiddenFeedIds: [],
1816
+ hideAllFeeds: false,
1817
+ },
1726
1818
  })
1727
1819
 
1728
1820
  await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
@@ -1789,6 +1881,10 @@ describe('agent', () => {
1789
1881
  verificationPrefs: {
1790
1882
  hideBadges: false,
1791
1883
  },
1884
+ liveEventPreferences: {
1885
+ hiddenFeedIds: [],
1886
+ hideAllFeeds: false,
1887
+ },
1792
1888
  })
1793
1889
 
1794
1890
  await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' })
@@ -1855,6 +1951,10 @@ describe('agent', () => {
1855
1951
  verificationPrefs: {
1856
1952
  hideBadges: false,
1857
1953
  },
1954
+ liveEventPreferences: {
1955
+ hiddenFeedIds: [],
1956
+ hideAllFeeds: false,
1957
+ },
1858
1958
  })
1859
1959
 
1860
1960
  await agent.setFeedViewPrefs('home', {
@@ -1932,6 +2032,10 @@ describe('agent', () => {
1932
2032
  verificationPrefs: {
1933
2033
  hideBadges: false,
1934
2034
  },
2035
+ liveEventPreferences: {
2036
+ hiddenFeedIds: [],
2037
+ hideAllFeeds: false,
2038
+ },
1935
2039
  })
1936
2040
 
1937
2041
  const res = await agent.app.bsky.actor.getPreferences()
@@ -3676,6 +3780,82 @@ describe('agent', () => {
3676
3780
  })
3677
3781
  })
3678
3782
 
3783
+ describe('updateLiveEventPreferences', () => {
3784
+ let agent: AtpAgent
3785
+
3786
+ beforeAll(async () => {
3787
+ agent = new AtpAgent({ service: network.pds.url })
3788
+
3789
+ await agent.createAccount({
3790
+ handle: 'live-event-prefs.test',
3791
+ email: 'live-event-prefs@test.com',
3792
+ password: 'password',
3793
+ })
3794
+ })
3795
+
3796
+ it('default state', async () => {
3797
+ const prefs = await agent.getPreferences()
3798
+ expect(prefs.liveEventPreferences).toEqual({
3799
+ hiddenFeedIds: [],
3800
+ hideAllFeeds: false,
3801
+ })
3802
+ })
3803
+
3804
+ it('hideFeed adds a feed id', async () => {
3805
+ await agent.updateLiveEventPreferences({
3806
+ type: 'hideFeed',
3807
+ id: 'feed1',
3808
+ })
3809
+ const prefs = await agent.getPreferences()
3810
+ expect(prefs.liveEventPreferences).toEqual({
3811
+ hiddenFeedIds: ['feed1'],
3812
+ hideAllFeeds: false,
3813
+ })
3814
+ })
3815
+
3816
+ it('hideFeed adds another feed id', async () => {
3817
+ await agent.updateLiveEventPreferences({
3818
+ type: 'hideFeed',
3819
+ id: 'feed2',
3820
+ })
3821
+ const prefs = await agent.getPreferences()
3822
+ expect(prefs.liveEventPreferences).toEqual({
3823
+ hiddenFeedIds: ['feed1', 'feed2'],
3824
+ hideAllFeeds: false,
3825
+ })
3826
+ })
3827
+
3828
+ it('unhideFeed removes a feed id', async () => {
3829
+ await agent.updateLiveEventPreferences({
3830
+ type: 'unhideFeed',
3831
+ id: 'feed1',
3832
+ })
3833
+ const prefs = await agent.getPreferences()
3834
+ expect(prefs.liveEventPreferences).toEqual({
3835
+ hiddenFeedIds: ['feed2'],
3836
+ hideAllFeeds: false,
3837
+ })
3838
+ })
3839
+
3840
+ it('toggleHideAllFeeds toggles the flag', async () => {
3841
+ await agent.updateLiveEventPreferences({ type: 'toggleHideAllFeeds' })
3842
+ const prefs = await agent.getPreferences()
3843
+ expect(prefs.liveEventPreferences).toEqual({
3844
+ hiddenFeedIds: ['feed2'],
3845
+ hideAllFeeds: true,
3846
+ })
3847
+ })
3848
+
3849
+ it('toggleHideAllFeeds toggles back', async () => {
3850
+ await agent.updateLiveEventPreferences({ type: 'toggleHideAllFeeds' })
3851
+ const prefs = await agent.getPreferences()
3852
+ expect(prefs.liveEventPreferences).toEqual({
3853
+ hiddenFeedIds: ['feed2'],
3854
+ hideAllFeeds: false,
3855
+ })
3856
+ })
3857
+ })
3858
+
3679
3859
  // end
3680
3860
  })
3681
3861
  })
@@ -93,6 +93,10 @@ describe('agent', () => {
93
93
  verificationPrefs: {
94
94
  hideBadges: false,
95
95
  },
96
+ liveEventPreferences: {
97
+ hiddenFeedIds: [],
98
+ hideAllFeeds: false,
99
+ },
96
100
  })
97
101
  })
98
102
 
@@ -149,6 +153,10 @@ describe('agent', () => {
149
153
  verificationPrefs: {
150
154
  hideBadges: false,
151
155
  },
156
+ liveEventPreferences: {
157
+ hiddenFeedIds: [],
158
+ hideAllFeeds: false,
159
+ },
152
160
  })
153
161
  expect(agent.labelers).toStrictEqual(['did:plc:other'])
154
162
 
@@ -190,6 +198,10 @@ describe('agent', () => {
190
198
  verificationPrefs: {
191
199
  hideBadges: false,
192
200
  },
201
+ liveEventPreferences: {
202
+ hiddenFeedIds: [],
203
+ hideAllFeeds: false,
204
+ },
193
205
  })
194
206
  expect(agent.labelers).toStrictEqual([])
195
207
  })
@@ -253,6 +265,10 @@ describe('agent', () => {
253
265
  verificationPrefs: {
254
266
  hideBadges: false,
255
267
  },
268
+ liveEventPreferences: {
269
+ hiddenFeedIds: [],
270
+ hideAllFeeds: false,
271
+ },
256
272
  })
257
273
  })
258
274
 
@@ -371,6 +371,74 @@ describe('detectFacets', () => {
371
371
  expect(detectedIndices).toEqual(indices)
372
372
  })
373
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
+ })
374
442
  })
375
443
 
376
444
  function segmentToOutput(segment: RichTextSegment): string[] {