@@ -12,6 +12,22 @@ const linkValues = (text: string) => links(text).map((l) => l.value);
1212/** Resolved URLs of the link segments. */
1313const linkUrls = ( text : string ) => links ( text ) . map ( ( l ) => l . url ) ;
1414
15+ /** Just the alias segments. */
16+ const aliases = ( text : string ) =>
17+ parseTextLinks ( text ) . filter (
18+ ( s ) : s is Extract < TextSegment , { type : 'alias' } > => s . type === 'alias' ,
19+ ) ;
20+
21+ /** Just the identity segments. */
22+ const identities = ( text : string ) =>
23+ parseTextLinks ( text ) . filter (
24+ ( s ) : s is Extract < TextSegment , { type : 'identity' } > =>
25+ s . type === 'identity' ,
26+ ) ;
27+
28+ const HEX64 =
29+ '0a2abecb223dbd572729018f8d201f32471e2a5b71e2032c052f6830846c4722' ;
30+
1531describe ( 'parseTextLinks' , ( ) => {
1632 describe ( 'no links' , ( ) => {
1733 it ( 'returns a single text segment for plain text' , ( ) => {
@@ -143,6 +159,90 @@ describe('parseTextLinks', () => {
143159 } ) ;
144160 } ) ;
145161
162+ describe ( 'alias mentions' , ( ) => {
163+ it ( 'detects an `@user@domain.com` mention' , ( ) => {
164+ expect ( parseTextLinks ( '@user@domain.com' ) ) . toEqual ( [
165+ { type : 'alias' , value : '@user@domain.com' , alias : 'user@domain.com' } ,
166+ ] ) ;
167+ } ) ;
168+
169+ it ( 'detects a mention within surrounding text' , ( ) => {
170+ expect ( parseTextLinks ( 'hey @user@domain.com bye' ) ) . toEqual ( [
171+ { type : 'text' , value : 'hey ' } ,
172+ { type : 'alias' , value : '@user@domain.com' , alias : 'user@domain.com' } ,
173+ { type : 'text' , value : ' bye' } ,
174+ ] ) ;
175+ } ) ;
176+
177+ it ( 'excludes trailing punctuation from the mention' , ( ) => {
178+ expect ( parseTextLinks ( 'see @user@domain.com.' ) ) . toEqual ( [
179+ { type : 'text' , value : 'see ' } ,
180+ { type : 'alias' , value : '@user@domain.com' , alias : 'user@domain.com' } ,
181+ { type : 'text' , value : '.' } ,
182+ ] ) ;
183+ } ) ;
184+
185+ it ( 'preserves case (normalisation happens downstream)' , ( ) => {
186+ expect ( aliases ( '@User@Domain.com' ) ) . toEqual ( [
187+ { type : 'alias' , value : '@User@Domain.com' , alias : 'User@Domain.com' } ,
188+ ] ) ;
189+ } ) ;
190+
191+ it ( 'allows dotted/underscored local parts' , ( ) => {
192+ expect ( aliases ( '@first.last_1@domain.io' ) ) . toEqual ( [
193+ {
194+ type : 'alias' ,
195+ value : '@first.last_1@domain.io' ,
196+ alias : 'first.last_1@domain.io' ,
197+ } ,
198+ ] ) ;
199+ } ) ;
200+
201+ it ( 'does not treat a plain email as a mention' , ( ) => {
202+ expect ( aliases ( 'reach me@example.com please' ) ) . toEqual ( [ ] ) ;
203+ } ) ;
204+
205+ it ( 'does not match `@user@` with no TLD' , ( ) => {
206+ expect ( aliases ( '@user@localhost here' ) ) . toEqual ( [ ] ) ;
207+ } ) ;
208+ } ) ;
209+
210+ describe ( 'identity mentions' , ( ) => {
211+ it ( 'detects an `@<64-hex>` mention' , ( ) => {
212+ expect ( parseTextLinks ( `@${ HEX64 } ` ) ) . toEqual ( [
213+ { type : 'identity' , value : `@${ HEX64 } ` , identity : HEX64 } ,
214+ ] ) ;
215+ } ) ;
216+
217+ it ( 'detects a mention within surrounding text' , ( ) => {
218+ expect ( parseTextLinks ( `hi @${ HEX64 } ok` ) ) . toEqual ( [
219+ { type : 'text' , value : 'hi ' } ,
220+ { type : 'identity' , value : `@${ HEX64 } ` , identity : HEX64 } ,
221+ { type : 'text' , value : ' ok' } ,
222+ ] ) ;
223+ } ) ;
224+
225+ it ( 'excludes trailing punctuation from the mention' , ( ) => {
226+ expect ( parseTextLinks ( `see @${ HEX64 } .` ) ) . toEqual ( [
227+ { type : 'text' , value : 'see ' } ,
228+ { type : 'identity' , value : `@${ HEX64 } ` , identity : HEX64 } ,
229+ { type : 'text' , value : '.' } ,
230+ ] ) ;
231+ } ) ;
232+
233+ it ( 'does not match fewer than 64 hex chars' , ( ) => {
234+ expect ( identities ( '@deadbeef here' ) ) . toEqual ( [ ] ) ;
235+ } ) ;
236+
237+ it ( 'does not match a longer hex run (not exactly 64)' , ( ) => {
238+ expect ( identities ( `@${ HEX64 } ab` ) ) . toEqual ( [ ] ) ;
239+ } ) ;
240+
241+ it ( 'does not match 64 non-hex chars' , ( ) => {
242+ expect ( identities ( `@${ 'g' . repeat ( 64 ) } here` ) ) . toEqual ( [ ] ) ;
243+ } ) ;
244+ } ) ;
245+
146246 describe ( 'multiple links & surrounding text' , ( ) => {
147247 it ( 'detects several links with text between them' , ( ) => {
148248 const segs = parseTextLinks (
@@ -185,6 +285,8 @@ describe('parseTextLinks', () => {
185285 'see https://example.com/path?x=1#y now' ,
186286 '(www.example.com), and example.org. done' ,
187287 'email a@b.com plus https://c.io end' ,
288+ 'hey @user@domain.com and a@b.com and example.net' ,
289+ `mention @${ HEX64 } mid sentence` ,
188290 'multi https://x.com www.y.org example.net z' ,
189291 '' ,
190292 ] ) ( 'rejoining all segment values reproduces the input: %s' , ( input ) => {
0 commit comments