@atproto/api 0.20.23 β 0.20.26
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.
- package/CHANGELOG.md +34 -0
- package/dist/agent.d.ts +4 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +17 -0
- package/dist/agent.js.map +1 -1
- package/dist/client/lexicons.d.ts +8 -0
- package/dist/client/lexicons.d.ts.map +1 -1
- package/dist/client/lexicons.js +4 -0
- package/dist/client/lexicons.js.map +1 -1
- package/dist/client/types/app/bsky/actor/defs.d.ts +2 -0
- package/dist/client/types/app/bsky/actor/defs.d.ts.map +1 -1
- package/dist/client/types/app/bsky/actor/defs.js.map +1 -1
- package/dist/rich-text/detection.d.ts.map +1 -1
- package/dist/rich-text/detection.js +4 -1
- package/dist/rich-text/detection.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +22 -16
- package/definitions/labels.json +0 -170
- package/docs/moderation.md +0 -260
- package/jest.config.cjs +0 -23
- package/jest.d.ts +0 -20
- package/jest.setup.ts +0 -97
- package/scripts/code/labels.mjs +0 -77
- package/scripts/generate-code.mjs +0 -4
- package/src/age-assurance.test.ts +0 -218
- package/src/age-assurance.ts +0 -137
- package/src/agent.ts +0 -1645
- package/src/atp-agent.ts +0 -570
- package/src/bsky-agent.ts +0 -14
- package/src/const.ts +0 -1
- package/src/index.ts +0 -44
- package/src/mocker.ts +0 -220
- package/src/moderation/decision.ts +0 -389
- package/src/moderation/index.ts +0 -69
- package/src/moderation/mutewords.ts +0 -170
- package/src/moderation/subjects/account.ts +0 -44
- package/src/moderation/subjects/feed-generator.ts +0 -24
- package/src/moderation/subjects/notification.ts +0 -25
- package/src/moderation/subjects/post.ts +0 -433
- package/src/moderation/subjects/profile.ts +0 -26
- package/src/moderation/subjects/status.ts +0 -27
- package/src/moderation/subjects/user-list.ts +0 -48
- package/src/moderation/types.ts +0 -191
- package/src/moderation/ui.ts +0 -21
- package/src/moderation/util.ts +0 -111
- package/src/predicate.ts +0 -52
- package/src/rich-text/detection.ts +0 -146
- package/src/rich-text/rich-text.ts +0 -418
- package/src/rich-text/sanitization.ts +0 -42
- package/src/rich-text/unicode.ts +0 -47
- package/src/rich-text/util.ts +0 -15
- package/src/session-manager.ts +0 -5
- package/src/types.ts +0 -165
- package/src/util.ts +0 -96
- package/tests/atp-agent.test.ts +0 -3863
- package/tests/dispatcher.test.ts +0 -527
- package/tests/errors.test.ts +0 -29
- package/tests/moderation-behaviors.test.ts +0 -951
- package/tests/moderation-custom-labels.test.ts +0 -334
- package/tests/moderation-mutewords.test.ts +0 -1299
- package/tests/moderation-prefs.test.ts +0 -402
- package/tests/moderation-quoteposts.test.ts +0 -277
- package/tests/moderation.test.ts +0 -739
- package/tests/rich-text-detection.test.ts +0 -456
- package/tests/rich-text-sanitization.test.ts +0 -216
- package/tests/rich-text.test.ts +0 -705
- package/tests/util/echo-server.ts +0 -37
- package/tests/util/moderation-behavior.ts +0 -268
- package/tsconfig.build.json +0 -9
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- 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
|
-
}
|