Search code examples
regexreact-native

How do replace urls and youtube urls and replaces the urls as anchor tags and replace youtube urls as Iframe tags


Say if the string returns:

str = `
            https://www.google.com
            http://google.com
            https://www.youtube.com/live/gNIQWYgf-0
            https://www.youtube.com/embed/3ul2LYG6j14?si=fgxYHjyt6zBmoYEr
            https://youtu.be/75Dhfjf6hfjfj
            this also has to take into account the variations of different youtube urls

      `

so when this string comes back I want to wrap the anchor tags for:

<a href="https://www.google.com">https://www.google.com</a>
<a href="https://google.com">https://www.google.com</a>

and wrap the YouTube with iframe tags:

<iframe width="420" height="315" src="https://youtu.be/75Dhfjf6hfjfj"></iframe>
<iframe width="420" height="315" src="https://www.youtube.com/embed/3ul2LYG6j14?si=fgxYHjyt6zBmoYEr"></iframe>

How can I achieve this, what I have tried:

  const redExpYoutube = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|\?v=|watch\?v=|&v=)([^#&?]*).*/;
  const regExpUrl = /(www+\.)?(?!youtube?!youtu.be)([\w-]+s{0,3})[/\.,;:!]{1,3}\s{0,3}(r[o0]|n[e3]t|lt|c[o0]m|[i!]nf[o0]|[o0]rg|b[i!][z2]|ru|[e3]du)(\/)?/g;

    embeddedString = str.replace(regExpUrl, "<a style='text-decoration:underline;color:red;' href='$1' target='_blank'>$1</a>");
    
    console.log("1: " + embeddedString);

    embeddedString = embeddedString.replace(redExpYoutube, "<iframe src='https://$1$2/$3'></iframe>");
    
    embeddedString = embeddedString.replace(/<[\/]{0,1}(div)[^><]*>/g,"");
    
    console.log("2: " + embeddedString);

UPADTE


Update after Brett Donald answered but did not include the full scenario:

 str = `
            <div>https://www.google.com</div>
            <div><span>http://google.com</span></div>
            <div>https://www.youtube.com/live/gNIQWYgf-0</div>
            <div>https://www.youtube.com/embed/3ul2LYG6j14?si=fgxYHjyt6zBmoYEr</div>
            <div>https://youtu.be/75Dhfjf6hfjfj</div>
            this also has to take into account the variations of different youtube urls

      `

So the URLs will be wrapped around other elements as well as the YouTube URLs will have other elements wrapped around them.

<KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : "height"}   style={{ flex: 1, }}>
                        <RichEditor style={{ flex: 1, }}
                        
                        placeholder="Enter Comment..."
                            ref={richText}
                            initialContentHTML={editorHtml }
                            onChange ={ (txt)=> {editorOnChange(txt)}}
                        />
                    </KeyboardAvoidingView>

const editorOnChange = (txt) => {
    embeddedString = linkify(txt);
    console.log("2: " + embeddedString);
  }

UPDATE


I am using a WebView now to send data from WebView to react native call-back and do the manipulating there.

Im using the function in React native now and using a webview to pass the data back to the React native app and do the url manipulation there using window.ReactNativeWebView.postMessage(content);

<WebView 
    onMessage={(event) => {

        
        editorOnChange(event.nativeEvent.data);
      }}
    
    source={{
            html: `

                    
                    <!DOCTYPE html>
                    <html>
                        <head>
                            <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
                            
                        </head>
                        <body>
                        <style>
                                [contentEditable=true]:empty:not(:focus):before {
                                    content: attr(data-text);
                                    cursor: text;
                                    color: red;
                                    pointer-events:none;
                                }

                                #placeholder{
                                    position:absolute;
                                    
                                    color:#BDBDBD;font-size:46px;outline:none;
                                }
                                #contentHtml{
                                    position:absolute;
                                    color:black;font-size:46px;outline:none;width:100%;
                                }
                            </style>
                            <div id="contentHtml" contenteditable=true>
                            </div>
                            <div id="placeholder">
                                Enter Comment...
                            </div>
                        </body>
                    </html>
                    <script>
                        document.addEventListener("DOMContentLoaded", function() {
                            $("#placeholder").on("click", function(){
                                $("#contentHtml").focus();
                                $(this).hide();
                            })        
                        
                            $("#contentHtml").on("input", function(){
                                var content = $(this).text();
                                window.ReactNativeWebView.postMessage(content);
                                if($(this).html().length === 0){
                                    $("#placeholder").show();
                                } else {
                                    $("#placeholder").hide();
                                }
                            });
                        });
                        
                    </script>
                    
                    `,
        }}
        style={{
            width: '100%',
            height: '100%',
            fontSize:46,
        }}>



    </WebView>

This call-back in react native

  const editorOnChange = (txt) => {
    convertLinks(txt);
    //console.log(convertLinks(txt));
  }

This is the function that Brett Donald suggested to look at:

const convertLinks = ( input ) => {

    let text = input;
    const linksFound = text.match( /(?:www|http?|https?)[^\s]+/g );
    const aLink = [];
    console.log(linksFound);
    if ( linksFound != null ) {
  
      for ( let i=0; i<linksFound.length; i++ ) {
        let replace = linksFound[i];
        if ( !( linksFound[i].match( /(http(s?)):\/\// ) ) ) { replace = 'http://' + linksFound[i] }
        let linkText = replace.split( '/' )[2];
        if ( linkText.substring( 0, 3 ) == 'www' ) { linkText = linkText.replace( 'www.', '' ) }
        if ( linkText.match( /youtu/ ) ) {
  
          let youtubeID = replace.split( '/' ).slice(-1)[0];
          aLink.push( '<div class="video-wrapper"><iframe src="https://www.youtube.com/embed/' + youtubeID + '" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>' )
        }
        else if ( linkText.match( /vimeo/ ) ) {
          let vimeoID = replace.split( '/' ).slice(-1)[0];
          aLink.push( '<div class="video-wrapper"><iframe src="https://player.vimeo.com/video/' + vimeoID + '" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div>' )
        }
        else {
          aLink.push( '<a href="' + replace + '" target="_blank">' + linkText + '</a>' );
        }
        text = text.split( linksFound[i] ).map(item => { return aLink[i].includes('iframe') ? item.trim() : item } ).join( aLink[i] );
      }
      return text;
  
    }
    else {
      return input;
    }
  }

but when the linkify function is called the matches are concatenating here ["https://youtu.be/75fDo2yk6RM?si=QxkLmcT_TiIgHiTfhttp://www.google.comhGhy"], i cant seem to understand how to split the two matches. this line const linksFound = text.match( /(?:www|http?|https?)[^\s]+/g );

UPDATE


I found how to split the urls using text.match( /(?:www|http?|https?)[^\s]+/img )

But now the urls are wrapping correctly, but the result is leaving empty spaces like this:

  ["http://www.google.com", "https://youtu.be/75fDo2yk6RM?si=QxkLmcT_TiIgHiTf"]
  <a href="http://www.google.com" target="_blank">google.com</a> 
  <div class="video-wrapper"><iframe src="https://www.youtube.com/em
  
    
  
  ---all this empty space---
  
    
  
 
  
                                                                                                                                      
   mbed/75fDo2yk6RM?si=QxkLmcT_TiIgHiTf" frameborder="0" 
   allow="accelerometer; autoplay; encrypted-media; gyroscope; 
   picture-in-picture" allowfullscreen></iframe></div>

Solution

  • You need to start with some good linkify code, and build up a solution from there. I’ve written a basic formatYouTube() function here, which you can extend to cater for more scenarios.

    const linkify = t => {
      const isValidHttpUrl = s => {
        let u
        try {u = new URL(s)}
        catch (_) {return false}
        return u.protocol.startsWith("http")
      }
      const formatYouTube = u => {
        if (!u.includes('youtu')) return null
        if (u.includes('youtu.be'))
          return `https://www.youtube.com/embed/${u.match(/[^\/]*$/)[0]}`
        return u
      }
      const m = t.match(/(?<=\s|^)[a-zA-Z0-9-:/]+\.[a-zA-Z0-9-].+?(?=[.,;:?!-]?(?:\s|$))/g)
      if (!m) return t
      const a = []
      m.forEach(x => {
        const [t1, ...t2] = t.split(x)
        a.push(t1)
        t = t2.join(x)
        const y = (!(x.match(/:\/\//)) ? 'https://' : '') + x
        if (isNaN(x) && isValidHttpUrl(y)) {
          const z = formatYouTube(y)
          if (z)
            a.push('<iframe src="' + z + '"></iframe>')
          else
            a.push('<a href="' + y + '" target="_blank">' + y.split('/')[2] + '</a>')
        }
        else
          a.push(x)
      })
      a.push(t)
      return a.join('')
    }
    
    const b = document.querySelector('button')
    const ta = document.querySelector('textarea')
    const o = document.querySelector('.output')
    
    b.addEventListener('click', evt => {
      o.innerHTML = linkify(ta.innerHTML).replaceAll('\n','<br>')
    })
    body, textarea, button {
      font-family: monospace;
    }
    body {
      margin: 1em;
      background: silver;
    }
    textarea {
      display: block;
      height: 9em;
      width: 100%;
      padding: 1em;
      box-sizing: border-box;
    }
    button {
      margin: 1em 0;
      padding: 0.5em 1em;
      border: 1px solid black;
      background: black;
      color: white;
      cursor: pointer;
    }
    .output {
      border: 1px solid black;
      width: 100%;
      padding: 1em;
      box-sizing: border-box;
      background: white; 
    }
    iframe {
      margin: 1em; 0;
      border: 1px solid black;
    }
    <textarea>
    one two three microsoft.com four five six
    https://www.google.com or http://google.com
    https://www.youtube.com/live/gNIQWYgf-0
    https://www.youtube.com/embed/3ul2LYG6j14?si=fgxYHjyt6zBmoYEr
    https://youtu.be/75Dhfjf6hfjfj
    </textarea>
    <button>Linkify</button>
    <div class="output"></div>

    After running this snippet, use the full page link to see things properly.