@@ -751,4 +751,113 @@ component extends="wheels.Controller" {
751751 rethrow ;
752752 }
753753 }
754+
755+ /**
756+ * Extracts YouTube video ID from various YouTube URL formats
757+ * Supports: https://youtube.com/watch?v=xyz, https://youtu.be/xyz, https://www.youtube.com/embed/xyz
758+ */
759+ string function extractYouTubeId (required string url ) {
760+ var id = " " ;
761+ // youtu.be format
762+ if (findNoCase (" youtu.be/" , arguments .url )) {
763+ id = listLast (arguments .url , " /" );
764+ // Remove any query parameters
765+ if (findNoCase (" ?" , id )) {
766+ id = listFirst (id , " ?" );
767+ }
768+ }
769+ // youtube.com/watch?v= format
770+ else if (findNoCase (" youtube.com" , arguments .url ) && findNoCase (" v=" , arguments .url )) {
771+ var params = listLast (arguments .url , " ?" );
772+ var paramList = listToArray (params , " &" );
773+ for (var param in paramList ) {
774+ if (findNoCase (" v=" , param )) {
775+ id = listLast (param , " =" );
776+ break ;
777+ }
778+ }
779+ }
780+ // Already embed format
781+ else if (findNoCase (" youtube.com/embed/" , arguments .url )) {
782+ id = listLast (listFirst (arguments .url , " ?" ), " /" );
783+ }
784+ return trim (id );
785+ }
786+
787+ /**
788+ * Detects if a URL is embeddable and returns embed HTML
789+ * Supports: YouTube, Twitter
790+ */
791+ string function getEmbedHtml (required string url , string width = " 100%" , string height = " 400" ) {
792+ var embedHtml = " " ;
793+ var youtubeId = " " ;
794+ var vimeoId = " " ;
795+ var trimmedUrl = trim (arguments .url );
796+
797+ // YouTube
798+ if (findNoCase (" youtube.com" , trimmedUrl ) || findNoCase (" youtu.be" , trimmedUrl )) {
799+ youtubeId = extractYouTubeId (trimmedUrl );
800+ if (len (youtubeId )) {
801+ embedHtml = ' <iframe width="#arguments .width #" height="#arguments .height #" src="https://www.youtube.com/embed/#youtubeId #?rel=0" frameborder="0" allowfullscreen style="max-width: 100%; margin: 1rem 0; border-radius: 0.5rem;"></iframe>' ;
802+ }
803+ }
804+ // Twitter/X
805+ else if (findNoCase (" twitter.com" , trimmedUrl ) || findNoCase (" x.com" , trimmedUrl )) {
806+ embedHtml = ' <blockquote class="twitter-tweet" style="margin: 1rem 0;"><a href="#trimmedUrl #"></a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>' ;
807+ }
808+
809+ return embedHtml ;
810+ }
811+
812+ /**
813+ * Checks if a URL is embeddable
814+ */
815+ boolean function isEmbeddableUrl (required string url ) {
816+ var embeddableDomains = [" youtube.com" , " youtu.be" , " twitter.com" , " x.com" ];
817+ var trimmedUrl = lcase (trim (arguments .url ));
818+
819+ for (var domain in embeddableDomains ) {
820+ if (findNoCase (domain , trimmedUrl )) {
821+ return true ;
822+ }
823+ }
824+ return false ;
825+ }
826+
827+ /**
828+ * Converts plain text URLs and embeddable links into HTML
829+ * For embeddable URLs (YouTube, Vimeo, etc.), creates embed iframes
830+ * For other URLs, creates anchor tags
831+ */
832+ string function embedAndAutoLink (required string content , string class = " text--primary" , string target = " _blank" ) {
833+ var result = arguments .content ;
834+ var urlPattern = " (https?://[^\s<"" '`]+)" ;
835+ var matches = reMatch (urlPattern , result );
836+
837+ // Remove duplicates
838+ var uniqueUrls = {};
839+ for (var match in matches ) {
840+ var cleanUrl = trim (match );
841+ // Skip if it's already part of an href or src
842+ if (! findNoCase (" href='#cleanUrl #" , result ) && ! findNoCase (' href="' & cleanUrl & ' "' , result ) && ! findNoCase (" src='#cleanUrl #" , result ) && ! findNoCase (' src="' & cleanUrl & ' "' , result )) {
843+ uniqueUrls [cleanUrl ] = cleanUrl ;
844+ }
845+ }
846+
847+ // Replace each unique URL
848+ for (var link in uniqueUrls ) {
849+ if (isEmbeddableUrl (link )) {
850+ var embedCode = getEmbedHtml (link );
851+ if (len (embedCode )) {
852+ result = replace (result , link , embedCode , " all" );
853+ }
854+ } else {
855+ // Regular link
856+ var linkHtml = ' <a href="' & link & ' " class="' & arguments .class & ' " target="' & arguments .target & ' ">' & link & ' </a>' ;
857+ result = replace (result , link , linkHtml , " all" );
858+ }
859+ }
860+
861+ return result ;
862+ }
754863}
0 commit comments