const PLATFORM_CONFIGS = {
calendly: {
name: 'Calendly',
urlRegexes: [
/^(?:https?:\/\/)?(?:www\.)?calendly\.com\/([a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)?)(?:\/?(?:\?.+)?)?$/i,
],
handleValidationRegex: /^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)?$/,
urlTemplate: ( identifier ) => `https://calendly.com/${ identifier }`,
normalizeIdentifier: ( id ) => id.toLowerCase(),
},
youtube: {
name: 'YouTube',
urlRegexes: [
/^(?:https?:\/\/)?(?:www\.)?youtube\.com\/(@[a-zA-Z0-9_.-]+)(?:\/?(?:\?.+)?)?$/i, // Handles @username
/^(?:https?:\/\/)?(?:www\.)?youtube\.com\/(channel\/UC[a-zA-Z0-9_-]+)(?:\/?(?:\?.+)?)?$/i, // Channel IDs UC...
/^(?:https?:\/\/)?(?:www\.)?youtube\.com\/(c\/[a-zA-Z0-9_.-]+)(?:\/?(?:\?.+)?)?$/i, // Custom URLs c/something
/^(?:https?:\/\/)?(?:www\.)?youtube\.com\/(user\/[a-zA-Z0-9_.-]+)(?:\/?(?:\?.+)?)?$/i, // Legacy user URLs user/something
],
// For direct handle input, we'll primarily support the @handle format or a plain username which we'll prefix with @
handleValidationRegex: /^@?[a-zA-Z0-9_.-]+$/,
urlTemplate: ( identifier ) => `https://youtube.com/${ identifier }`,
normalizeIdentifier: ( id ) => {
if ( id.startsWith( '@' ) || id.startsWith( 'channel/' ) || id.startsWith( 'c/' ) || id.startsWith( 'user/' ) || /^UC/.test( id ) ) {
return id;
}
return '@' + id;
},
},
wordpress: {
name: 'WordPress',
urlRegexes: [
/^(?:https?:\/\/)?profiles\.wordpress\.org\/([a-zA-Z0-9_.-]+)\/?(?:\/.*)?$/i,
],
handleValidationRegex: /^[a-zA-Z0-9_.-]+$/,
urlTemplate: ( identifier ) => `https://profiles.wordpress.org/${ identifier.toLowerCase() }/`,
normalizeIdentifier: ( id ) => id.toLowerCase(),
},
xitter: {
name: 'X',
urlRegexes: [
/^(?:https?:\/\/)?(?:www\.)?(?:twitter|x)\.com\/([a-zA-Z0-9_]{1,15})(?:\/?(?:\?.+)?)?$/i,
],
handleValidationRegex: /^[a-zA-Z0-9_]{1,15}$/,
urlTemplate: ( identifier ) => `https://x.com/${ identifier }`,
normalizeIdentifier: ( id ) => id.replace( /^@/, '' ),
},
facebook: {
name: 'Facebook',
urlRegexes: [
/^(?:https?:\/\/)?(?:www\.)?(?:facebook|fb)\.com\/(?:profile\.php\?id=)?(\d+)(?:&.+|\/?)$/i,
/^(?:https?:\/\/)?(?:www\.)?(?:facebook|fb)\.com\/(?!pages\/|groups\/|events\/|photo(?:s|\.php)?|permalink\.php|story\.php|watch\/?|live\/?|video(?:s|\.php)?|media\/?|messages\/|gaming\/|notes\/|sharer(?:\.php)?|login\.php|help\/|legal\/|marketplace\/|ads\/|posts\/|hashtag\/)([a-zA-Z0-9._-]+)(?:\/?(?:\?.*)?)?$/i,
],
handleValidationRegex: /^(?:[a-zA-Z0-9._-]+|\d+)$/,
urlTemplate: ( identifier, originalInputUrl ) => {
if ( /^\d+$/.test( identifier ) && originalInputUrl && /profile\.php\?id=/.test( originalInputUrl ) ) {
return `https://facebook.com/profile.php?id=${ identifier }`;
}
return `https://facebook.com/${ identifier }`;
},
normalizeIdentifier: ( id ) => id.replace( /^@/, '' ),
},
bluesky: {
name: 'Bluesky',
urlRegexes: [
/^(?:https?:\/\/)?(?:www\.)?bsky\.app\/profile\/([a-zA-Z0-9.-]+[a-zA-Z0-9])(?:\/?(?:\?.+)?)?$/i,
],
handleValidationRegex: /^[a-zA-Z0-9.-]+[a-zA-Z0-9]$/,
urlTemplate: ( identifier ) => `https://bsky.app/profile/${ identifier }`,
normalizeIdentifier: ( id ) => id.replace( /^@/, '' ),
finalizeIdentifier: ( id, wasHandleInputOnly ) => {
if ( wasHandleInputOnly && id && ! id.includes( '.' ) ) {
return `${ id }.bsky.social`;
}
return id;
},
},
tiktok: {
name: 'TikTok',
urlRegexes: [
/^(?:https?:\/\/)?(?:www\.)?tiktok\.com\/@([a-zA-Z0-9_.]+)(?:\/?(?:\?.+)?)?$/i,
],
handleValidationRegex: /^[a-zA-Z0-9_.]+$/,
urlTemplate: ( identifier ) => `https://tiktok.com/@${ identifier }`,
normalizeIdentifier: ( id ) => id.replace( /^@/, '' ),
},
whatsapp: {
name: 'WhatsApp',
urlRegexes: [
/^(?:https?:\/\/)?(?:wa\.me\/|api\.whatsapp\.com\/send\/?\?phone=)(\+?\d+[\d\s()-]*\d)(?:\/?(?:\?.+)?)?$/i,
],
handleValidationRegex: /^\+?\d+[\d\s()-]*\d$/,
urlTemplate: ( identifier ) => `https://wa.me/${ identifier.replace( /\D/g, '' ) }`,
normalizeIdentifier: ( id ) => id.replace( /\D/g, '' ),
},
threads: {
name: 'Threads',
urlRegexes: [
/^(?:https?:\/\/)?(?:www\.)?threads\.net\/@([a-zA-Z0-9_.]+)(?:\/?(?:\?.+)?)?$/i,
],
handleValidationRegex: /^[a-zA-Z0-9_.]+$/,
urlTemplate: ( identifier ) => `https://threads.net/@${ identifier }`,
normalizeIdentifier: ( id ) => id.replace( /^@/, '' ),
},
linkedin: {
name: 'LinkedIn',
urlRegexes: [
/^(?:https?:\/\/)?(?:www\.)?linkedin\.com\/in\/([a-zA-Z0-9_-]+)(?:\/?(?:\?.+)?)?$/i,
/^(?:https?:\/\/)?(?:www\.)?linkedin\.com\/company\/([a-zA-Z0-9_-]+)(?:\/?(?:\?.+)?)?$/i,
/^(?:https?:\/\/)?(?:www\.)?linkedin\.com\/school\/([a-zA-Z0-9_-]+)(?:\/?(?:\?.+)?)?$/i,
/^(?:https?:\/\/)?(?:www\.)?linkedin\.com\/showcase\/([a-zA-Z0-9_-]+)(?:\/?(?:\?.+)?)?$/i,
/^(?:https?:\/\/)?(?:www\.)?linkedin\.com\/pub\/([a-zA-Z0-9_-]+(?:-[a-zA-Z0-9_-]+)*)(?:\/[a-zA-Z0-9]+){0,3}\/?(?:\?.+)?$/i,
],
handleValidationRegex: /^[a-zA-Z0-9_-]+$/,
urlTemplate: ( identifier, originalInputUrl ) => {
const cleanId = identifier.split( '/' )[ 0 ];
if ( originalInputUrl ) {
if ( originalInputUrl.includes( '/company/' ) ) {
return `https://linkedin.com/company/${ cleanId }`;
}
if ( originalInputUrl.includes( '/school/' ) ) {
return `https://linkedin.com/school/${ cleanId }`;
}
if ( originalInputUrl.includes( '/showcase/' ) ) {
return `https://linkedin.com/showcase/${ cleanId }`;
}
if ( originalInputUrl.includes( '/pub/' ) ) {
return `https://linkedin.com/pub/${ cleanId }`;
}
}
return `https://linkedin.com/in/${ cleanId }`;
},
normalizeIdentifier: ( id ) => id.replace( /^@/, '' ),
},
savvycal: {
name: 'SavvyCal',
urlRegexes: [
/^(?:https?:\/\/)?(?:www\.)?savvycal\.com\/([a-zA-Z0-9_-]+)(?:\/[a-zA-Z0-9_-]+)?(?:\/?(?:\?.+)?)?$/i,
],
handleValidationRegex: /^[a-zA-Z0-9_-]+$/,
urlTemplate: ( identifier ) => `https://savvycal.com/${ identifier.split( '/' )[ 0 ] }`,
normalizeIdentifier: ( id ) => id.replace( /^@/, '' ),
},
github: {
name: 'GitHub',
urlRegexes: [
/^(?:https?:\/\/)?(?:www\.)?github\.com\/([a-zA-Z0-9_-]+)(?:\/?(?:\?.+)?)?$/i,
],
handleValidationRegex: /^[a-zA-Z0-9_-]+$/,
urlTemplate: ( identifier ) => `https://github.com/${ identifier }`,
normalizeIdentifier: ( id ) => id.replace( /^@/, '' ),
},
instagram: {
name: 'Instagram',
urlRegexes: [
/^(?:https?:\/\/)?(?:www\.)?instagram\.com\/([a-zA-Z0-9_.]+)(?:\/?(?:\?.+)?)?$/i,
],
handleValidationRegex: /^[a-zA-Z0-9_.]+$/,
urlTemplate: ( identifier ) => `https://instagram.com/${ identifier }`,
normalizeIdentifier: ( id ) => id.replace( /^@/, '' ),
},
};
const SUPPORTED_PLATFORM_KEYS = Object.freeze( [
'calendly',
'youtube',
'wordpress',
'xitter',
'facebook',
'bluesky',
'tiktok',
'whatsapp',
'threads',
'linkedin',
'savvycal',
'github',
'instagram',
] );
/**
* @module parseSocial
* @description A module that parses a string that may be a URL or a social media handle.
* It returns a url, platform key, and an identifier, plus a valid key that is true or false.
*
* @since 4.0.6
*
* @param {string} input The input string that may be a URL or a social media handle. Defaults to an empty string.
* @param {string} [platformHint] Optional. The key of the platform (e.g., 'xitter', 'facebook') to help identify the input if it's a handle.
*
* @return {object} An object with the following properties, url, identifier, platform, and valid.
*
* @example
* import { parseSocial } from '@gravityforms/utils';
*
* // Example 1: Parsing a URL
* const twitterProfile = parseSocial('https://twitter.com/elonmusk');
* // twitterProfile: { url: 'https://x.com/elonmusk', identifier: 'elonmusk', platform: 'xitter', valid: true }
*
* // Example 2: Parsing a handle with a hint
* const facebookHandle = parseSocial('zuck', 'facebook');
* // facebookHandle: { url: 'https://facebook.com/zuck', identifier: 'zuck', platform: 'facebook', valid: true }
*
* // Example 3: Parsing an ambiguous handle without a hint (likely invalid)
* const ambiguousHandle = parseSocial('username123');
* // ambiguousHandle: { url: '', identifier: '', platform: '', valid: false }
*
* // Example 4: Invalid input
* const invalidInput = parseSocial('not a url or handle', 'randomPlatform');
* // invalidInput: { url: '', identifier: '', platform: '', valid: false }
*
*/
export default function parseSocial( input = '', platformHint = '' ) {
const result = {
url: '',
identifier: '',
platform: '',
valid: false,
};
if ( ! input || typeof input !== 'string' ) {
return result;
}
const trimmedInput = input.trim();
if ( ! trimmedInput ) {
return result;
}
const hintKey = platformHint ? platformHint.toLowerCase() : '';
for ( const key of SUPPORTED_PLATFORM_KEYS ) {
const config = PLATFORM_CONFIGS[ key ];
for ( const regex of config.urlRegexes ) {
const match = trimmedInput.match( regex );
if ( match && match[ 1 ] ) {
const rawIdentifier = match[ 1 ];
result.identifier = config.normalizeIdentifier( rawIdentifier );
if ( config.finalizeIdentifier ) {
result.identifier = config.finalizeIdentifier( result.identifier, false );
}
result.url = config.urlTemplate( result.identifier, trimmedInput );
result.platform = key;
result.valid = true;
return result;
}
}
}
if ( hintKey && SUPPORTED_PLATFORM_KEYS.includes( hintKey ) ) {
const config = PLATFORM_CONFIGS[ hintKey ];
const normalizedInputAsHandle = config.normalizeIdentifier( trimmedInput );
if ( config.handleValidationRegex && config.handleValidationRegex.test( normalizedInputAsHandle ) ) {
result.identifier = normalizedInputAsHandle;
if ( config.finalizeIdentifier ) {
result.identifier = config.finalizeIdentifier( result.identifier, true );
}
result.url = config.urlTemplate( result.identifier, null );
result.platform = hintKey;
result.valid = true;
return result;
}
}
return result;
}