data_parse-social.js

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;
}