Parse user input for urls, timestamps & hashtags with RegEX 🧠
Tags: javascript, regex, react, tutorial
Published: 2019-02-17 16:23:02

---
title: Parse user input for urls, timestamps & hashtags with RegEX 🧠
published: true
description: Use regular expressions to parse user input and turn plain text into links
tags: javascript, regex, react, tutorial 
cover_image: https://thepracticaldev.s3.amazonaws.com/i/nl8u3zq5b1ub3gr40hcj.gif
---

{% youtube Hwj0oR8uINM %}

[Video code-along version of tutorial](https://www.youtube.com/watch?v=Hwj0oR8uINM) 📽

I used to avoid *Regular Expressions* aka [RegEx](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) at all costs. Aside from not understanding how to use it I didn't see much purpose for it in my code. I suppose, to be fair, *RegEx* isn't exactly beginner friendly. Nevertheless, I now find myself looking for opportunities to use it. The truth is that *RegEx* can save a lot of development time and is a powerful tool. 

Recently, I have been focused on re-creating parts of **YouTube**, and I noticed something simple, yet cool, about video descriptions and comments. Users can enter urls, timestamps and hashtags and **YouTube** will parse the input and transform the text into links. Urls become external links, timestamps are links that seek the current video to a specific spot and hashtags become search terms to find related content. 

![youtube-description](https://thepracticaldev.s3.amazonaws.com/i/9isp1sdkdg2rmpl9wstc.png)

There are some good tools and sites out there to learn about this. Sometimes just googling `regex for <whatever>` will bring up some good **Stack Overflow**. [RegExr](https://regexr.com/) is really cool. You can create an account as save your expressions into a library of your own. Plus they break down each character and what it does, not to mention a database of community expressions. [Regular Expressions Info](https://www.regular-expressions.info/tutorial.html) has more detailed breakdowns of pretty much anything and everything related to **RegEx**.

Now this tutorial assumes you have already captured and stored the user input. That is the raw text we are parsing. From here we need to address a few things as we process the text into **HTML**.

1. Preserve the formatting of text - spacing, line breaks, etc
2. Fit the text into an **HTML** element
3. Parse text for urls, timestamps (*HH:MM:SS* format) and hashtags
4. Replace these with appropriate links, target and params if needed
5. Bonus: set the time of video, perform a search based on hashtag term

⚠ Disclaimer - all code examples will use **React** and/or **JSX** syntax and therefore **JavaScript**

Preserving the format is pretty easy. One option is **HTML** `pre` tag. `pre` is short for pre-formatted I think. 🤦‍♂️

```jsx
<pre>{description}</pre>
```
Another option is to use the `white-space` CSS property set to `pre`. We might as well use `pre-wrap`. Otherwise long lines of text will overflow their container.

```jsx
<div style={{whiteSpace: 'pre-wrap'}}>{description}</div>
```

Now we need to bust out the big guns 🔫. First we need to find, and somewhat understand the regular expressions involved. Here is a pretty standard expression to find `http/s` urls. It basically looks for `http://anything`, but it seems to do the trick. Note the `g` flag - matches all occurrences and the `i` flag that ignores case. It can also match `ftp` and `file` urls by using the OR operator in the first capture block.

```javascript
const reUrl = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi
```

The timestamp expression isn't quite as bad. Note that `(?:)?` sets up capture groups that are optional. `[0-5]` makes sense because when dealing with `HH:MM:SS` you won't see `01:90:90`, the highest minute or second can be 59. Anyways, this is set up to match `MM:SS` and `:SS` which is cool. This allow the user a little more flexibility in what they can use are time links.

```javascript
const reTime = /\s(?:(?:([01]?\d):)?([0-5]?\d))?:([0-5]?\d)\s/g
```

Ok, lets get down to the function itself. We are going to leverage the `replace` method on the `String` prototype. [String.prototype.replace](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) *can* take `RegEx` as the first argument and a function as the second. This callback function can receive many arguments, but the first is the matched text itself. This means we can use the original urls/time/hash in our replacement string. The idea is to replace our matches with the appropriate **HTML**. To keep things simple, we'll start with urls. This process in commonly called the `linkify` process. Get it? 🧠

```javascript
function linkify(text) {
    return text.replace(reUrl, url => `<a href="${url}" target="_blank">${url}</a>`)
}
```

I used an arrow function and returned a template string to save on space. Target set to `_blank` ensures that this page will be opened in a new window. [Template strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) you should probably know about by now. 

Dealing with the timestamps is a little more advanced. We are going to need a helper function and some additional logic to make them useful. Assume we have a video player, like **YouTube**, for this example. We want to display the timestamp link in `HH:MM:SS` format but we need to convert that value to seconds so we can set a search parameter and have a value that we can send to our player - The **HTML** video element has a property called `currentTime` which gets/sets the time of the video in...seconds! We also need the value of the url to our player's page on our site.

```javascript
function HHMMSStoSeconds(str) {
  var p = str.split(':')
  var s = 0
  var m = 1

  while (p.length > 0) {
    s += m * parseInt(p.pop(), 10)
    m *= 60
  }

  return s
}

function linkify(text) {
    const playerUrl = 'http://www.youtube.com/watch'
    return text.replace(reTime, time => {
        const seconds = HHMMSStoSeconds(time)
        return `<a href="${playerUrl}?time=${seconds}">{time}</a>`
    })
}
```

As a side note I really like the string to seconds function. Its been a while since i used a `while` loop. 🤓

Now when a user clicks a timestamp link we can implement some tricky logic in our **React** component to seek the video to the time specified in the link.

```javascript

class Player extends React.Component {
    
    componentDidMount() {
        const params = new URLSearchParams(window.location.search)
        const time = params.get('time')
        if(time) {
            this.video.currentTime = time
        }
    }

    render() {
        return <video ref={el=>this.video = el} src={src} />
    }
}
```

This may look weird because we are used to routing libraries, but it works. Learn about [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams). Using a `ref` is also key here. There are a feature of React that gives us access to the underlying DOM Node and all the built in APIs that go with it. [React Refs](https://reactjs.org/docs/refs-and-the-dom.html) and [HTML video/audio DOM](https://www.w3schools.com/tags/ref_av_dom.asp)... are helpful.

Hashtags work in a very similar way to timestamps. It is up to the developer to decide how to implement them into the UI. YouTube runs a search for anything related to hashtag term. The expression to match hashtags might look something like this.

```javascript
const reHash = /(?:\s|^)?#[A-Za-z0-9\-\.\_]+(?:\s|$)/g
```

This one is actually almost understandable. But we can break it down as follows.

```
(?: // start of non-capture group
\s  // match space character
|   // logical OR
^   // beginning of string
)   // end non-capture group
?   // match 0 or 1 of preceding
#   // match # character
[]  // enclosed character set
A-Z // capital A through Z
a-z // lowercase a through z
0-9 // digits 0 through 9
\-  // \ is an escape character matches -
+   // requires 1 or more match of preceding token
$   // end of string 
```

Now we can lump everything together into one big function. Of course everyone's needs are different but the following would be something like YouTube. This time I am passing a `video` object. This is just one way to do it. However, in my implementation I don't see much sense in making timestamp links if the time is greater than the duration of the video. Check out the `if/else` block, by returning the parameter to the callback function it is as if we ignore that specific match. Worthwhile.

```javascript
import HHMMSStoSeconds from './above-this'

const reUrl = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi
const reTime = /\s(?:(?:([01]?\d):)?([0-5]?\d))?:([0-5]?\d)\s/g
const reHash = /(?:\s|^)?#[A-Za-z0-9\-\.\_]+(?:\s|$)/g
const frontend = 'https://www.youtube.com'

export default function linkify(video) {
  return (
    video.description
      .replace(reUrl, url => `<a href="${url}" target="_blank">${url}</a>`)
      .replace(reTime, time => {
        const secs = HHMMSStoSeconds(time)
        if (secs > video.duration) {
          return time
        } else {
          return `<a href="${frontend}/watch?id=${video.id}&t=${secs}">${time}</a>`
        }
      })
      .replace(
        reHash,
        hash => `<a href="${frontend}/search?term=${hash.replace('#', '').trim()}">${hash}</a>`
      )
  )
}

```

So if you actually made it this far you for sure learned something. I figured it took me a good part of a day to figure all this stuff out and I had to pull from all kinds of different websites and searches. Why not put it all down in the same place. Naturally, there are probably more efficient or more thorough **RegEx** out there. But these seem to work well for my use case.

{% codesandbox 4rpovv5pkw module=/src/linkify.js %}

[Parser Tutorial](https://www.youtube.com/watch?v=Hwj0oR8uINM)

[Clone Component Series](https://www.youtube.com/watch?v=IJD33I0Ixck&list=PLh6ckU65UHatP5H8ujyQ8IlPC6G5n8A3b&index=2)

[My YouTube Channel](https://www.youtube.com/channel/UCZ86uFH5o_5yjjBEbi2oFSw)

[Library that does all this for you](http://markdown-it.github.io/linkify-it/)