@@ -20,6 +20,12 @@ export type DomainShortcuts = {
2020const escapeForRegex = ( input : string ) =>
2121 input . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
2222
23+ const DOMAIN_LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?" ;
24+ const HANDLE = `${ DOMAIN_LABEL } (?:\\.${ DOMAIN_LABEL } )+` ;
25+ // Bluesky profile URLs accept a DID in place of a handle, e.g.
26+ // `bsky.app/profile/did:plc:abc123` or `did:web:example.com`.
27+ const DID = "did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]" ;
28+
2329export type CreateSocialLinksConfig = {
2430 /**
2531 * Register extra domains against a platform with a known URL shape
@@ -33,81 +39,81 @@ export const createSocialLinks = (config: CreateSocialLinksConfig = {}) => {
3339
3440 const tumblrMatches : ProfileMatch [ ] = [
3541 {
36- match : "https?://www.tumblr.com/([a-z0-9-]+)/?.*" ,
42+ match : "https?://www\\ .tumblr\\ .com/([a-z0-9-]+)/?.*" ,
3743 // TODO: more may be necessary for things like extracting usernames
3844 group : 1 ,
3945 } ,
4046 // Must be last because it's a more general match, or www will be as a username
4147 {
42- match : "https?://([a-z0-9-]+).tumblr.com/?.*" ,
48+ match : "https?://([a-z0-9-]+)\\ .tumblr\\ .com/?.*" ,
4349 // TODO: more may be necessary for things like extracting usernames
4450 group : 1 ,
4551 } ,
4652 ] ;
4753 socialLinks . addProfile ( "tumblr" , tumblrMatches ) ;
4854 const kofiMatches : ProfileMatch [ ] = [
4955 {
50- match : "https?://ko-fi.com/([a-z0-9-_]+)" ,
56+ match : "https?://ko-fi\\ .com/([a-z0-9-_]+)" ,
5157 group : 1 ,
5258 } ,
5359 ] ;
5460 socialLinks . addProfile ( "ko-fi" , kofiMatches ) ;
5561
5662 const inprntMatches : ProfileMatch [ ] = [
5763 {
58- match : "https?://(?:www.)?inprnt.com/gallery/([a-z0-9-]+)/?" ,
64+ match : "https?://(?:www\\ .)?inprnt\\ .com/gallery/([a-z0-9-]+)/?" ,
5965 group : 1 ,
6066 } ,
6167 ] ;
6268 socialLinks . addProfile ( "inprnt" , inprntMatches ) ;
6369
6470 const neocitiesMatches : ProfileMatch [ ] = [
6571 {
66- match : "https?://([a-z0-9-]+).neocities.org" ,
72+ match : "https?://([a-z0-9-]+)\\ .neocities\\ .org" ,
6773 group : 1 ,
6874 } ,
6975 ] ;
7076 socialLinks . addProfile ( "neocities" , neocitiesMatches ) ;
7177
7278 const blueSkyMatches : ProfileMatch [ ] = [
7379 {
74- match : "https?://([a-z0-9-]+).bsky.(?:app|social)/?.*" ,
80+ match : "https?://([a-z0-9-]+)\\ .bsky\\ .(?:app|social)/?.*" ,
7581 group : 1 ,
7682 } ,
7783 {
78- match : " https?://bsky.(?:app|social)/profile/([a-z0-9-.]+ )/?.*" ,
84+ match : ` https?://bsky\\ .(?:app|social)/profile/(${ HANDLE } | ${ DID } )/?.*` ,
7985 group : 1 ,
8086 } ,
8187 ] ;
82- socialLinks . addProfile ( "bsky" , blueSkyMatches ) ;
88+ replaceMatches ( socialLinks , "bsky" , blueSkyMatches ) ;
8389
8490 const ao3Matches : ProfileMatch [ ] = [
8591 {
86- match : "https?://archiveofourown.org/users/([a-z0-9-]+)" ,
92+ match : "https?://archiveofourown\\ .org/users/([a-z0-9-]+)" ,
8793 group : 1 ,
8894 } ,
8995 ] ;
9096 socialLinks . addProfile ( "archiveofourown" , ao3Matches ) ;
9197
9298 const dreamwidthMatches : ProfileMatch [ ] = [
9399 {
94- match : "https?://([a-z0-9-]+).dreamwidth.org" ,
100+ match : "https?://([a-z0-9-]+)\\ .dreamwidth\\ .org" ,
95101 group : 1 ,
96102 } ,
97103 ] ;
98104 socialLinks . addProfile ( "dreamwidth" , dreamwidthMatches ) ;
99105
100106 const furaffinityMatches : ProfileMatch [ ] = [
101107 {
102- match : "https?://www.furaffinity.net/user/([a-z0-9-]+)" ,
108+ match : "https?://www\\ .furaffinity\\ .net/user/([a-z0-9-]+)" ,
103109 group : 1 ,
104110 } ,
105111 ] ;
106112 socialLinks . addProfile ( "furaffinity" , furaffinityMatches ) ;
107113
108114 const carrdMatches : ProfileMatch [ ] = [
109115 {
110- match : "https?://([a-z0-9-]+).carrd.co/?" ,
116+ match : "https?://([a-z0-9-]+)\\ .carrd\\ .co/?" ,
111117 group : 1 ,
112118 } ,
113119 ] ;
@@ -116,16 +122,19 @@ export const createSocialLinks = (config: CreateSocialLinksConfig = {}) => {
116122 const kickstarterMatches : ProfileMatch [ ] = [
117123 {
118124 // https://www.kickstarter.com/projects/essential-randomness/the-fujoshi-guide-to-web-development
119- match : "https?://www.kickstarter.com/projects/[a-z0-9-]+/([a-z0-9-]+)/?" ,
125+ match :
126+ "https?://www\\.kickstarter\\.com/projects/[a-z0-9-]+/([a-z0-9-]+)/?" ,
120127 group : 1 ,
121128 } ,
122129 ] ;
123130 socialLinks . addProfile ( "kickstarter" , kickstarterMatches ) ;
124131
125132 const npmMatches : ProfileMatch [ ] = [
126133 {
127- // https://www.npmjs.com/package/@bobaboard /ao3.js
128- match : "https?://www.npmjs.com/package/([a-z0-9-@]+/[a-z0-9-\\.]+)/?" ,
134+ // Scoped and unscoped packages, e.g.
135+ // `npmjs.com/package/@bobaboard/ao3.js` or `npmjs.com/package/social-links`.
136+ match :
137+ "https?://www\\.npmjs\\.com/package/((?:@[a-z0-9-._]+/)?[a-z0-9-._]+)/?" ,
129138 group : 1 ,
130139 } ,
131140 ] ;
@@ -137,20 +146,20 @@ export const createSocialLinks = (config: CreateSocialLinksConfig = {}) => {
137146 const gitHubMatches : ProfileMatch [ ] = [
138147 {
139148 // https://github.com/FujoWebDev/AO3.js
140- match : "https?://github.com/([a-z0-9-]+/[a-z0-9-\\.]+)/?" ,
149+ match : "https?://github\\ .com/([a-z0-9-]+/[a-z0-9-\\.]+)/?" ,
141150 group : 1 ,
142151 } ,
143152 {
144153 // https://github.com/orgs/FujoWebDev/
145- match : "https?://github.com/orgs/([a-z0-9-]+)/?" ,
154+ match : "https?://github\\ .com/orgs/([a-z0-9-]+)/?" ,
146155 group : 1 ,
147156 } ,
148157 ] ;
149158 appendMatches ( socialLinks , "github" , gitHubMatches ) ;
150159
151160 const xMatches : ProfileMatch [ ] = [
152161 {
153- match : "(?:https?://)?(?:www.)?x.com/@?([a-z0-9-\\.]+)/?.*" ,
162+ match : "(?:https?://)?(?:www\\ .)?x\\ .com/@?([a-z0-9-\\.]+)/?.*" ,
154163 group : 1 ,
155164 } ,
156165 ] ;
@@ -178,6 +187,15 @@ const appendMatches = (
178187 socialLinks . profiles . set ( platform , [ ...existing , ...matches ] ) ;
179188} ;
180189
190+ const replaceMatches = (
191+ socialLinks : SocialLinksLib ,
192+ platform : string ,
193+ matches : ProfileMatch [ ] ,
194+ ) => {
195+ // @ts -expect-error profiles is private on SocialLinks
196+ socialLinks . profiles . set ( platform , matches ) ;
197+ } ;
198+
181199// This top-level call is safe even under `"sideEffects": false` in package.json
182200// because the logic in `createSocialLinks` only mutates the returned
183201// object, and doesn't touch anything observable from outside the package.
0 commit comments