Regex: Don't match if match is inside a URL

Viewed 231

I am trying to match all the hashtags in a description. However URLs sometimes appear in descriptions and URLs can sometimes contain the hashtag symbol.

Using regex, how can I avoid selecting hashtags if they are part of a URL?

Additionally, if a hashtag starts with a digit I want to ignore it because it is most likely not a hashtag but a list item.

Here is my current regex:

(https?:\/\/\S+)|#(.*?)[\s]

It mostly does not work, it erroneously matches #1, http://example.com/#urlpart and https://example.com#urlpart?s=search.

2 Answers

JavaScript solution:

Match a URL and match and capture the hashtags that meet your requirements with

/https?:\/\/\S+|(#(?!\d)[^\s#]+)/ig

JS extraction demo:

const s = "regular text\n\n#1 0:33 - Cool thing <-- this should not match\n\nTag 2 and tag 3 should be treated as seperate hashtags\n#tag1 #tag2#tag3 \n\nHashtags inside URLs should be ignored\n\nhttp://example.com/#urlpart\n\nhttps://example.com#urlpart?s=search";
const regex = /https?:\/\/\S+|(#(?!\d)[^\s#]+)/ig;
const matches = [...s.matchAll(regex)];
console.log( Array.from(matches, x => x[1]).filter(Boolean) );

JS replacement demo:

const s = "regular text\n\n#1 0:33 - Cool thing <-- this should not match\n\nTag 2 and tag 3 should be treated as seperate hashtags\n#tag1 #tag2#tag3 \n\nHashtags inside URLs should be ignored\n\nhttp://example.com/#urlpart\n\nhttps://example.com#urlpart?s=search";
const regex = /https?:\/\/\S+|(#(?!\d)([^\s#]+))/ig;
console.log( s.replace(regex, (x,y,z) => y !== undefined ? `<a href="dir/${z}">${y}</a>` : `${x}` ) );

PCRE Solution

You may use

https?:\/\/\S+(*SKIP)(*F)|#(?!\d)[^\s#]+

See the regex demo

Details

  • https?:\/\/\S+(*SKIP)(*F) - match and skip URLs
  • | - or
  • #(?!\d)[^\s#]+ - match and return substrings matching
    • # - a # char
    • (?!\d)[^\s#]+ - any chunks of one or more chars other than # and whitespace not starting with a digit.

You can turn the group around and capture what you want to keep. Then start the match with any char except a digit or whitespace char.

The value is in capture group 1.

\bhttps?:\/\/\S+|(#[^\s\d#][^\s#]*)

Explanation

  • \bhttps?:\/\/\S+ match the url
  • | Or
  • ( Capture group 1
    • #[^\s\d#] Match # followed by any char except #, a digit or whitespace char
    • [^\s#]* Match 0+ times any char except # or a whitespace char
  • ) Close group 1

Regex demo

const regex = /\bhttps?:\/\/\S+|(#[^\s\d#][^\s#]*)/gm;
const str = `regular text

#1 0:33 - Cool thing <-- this should not match

Tag 2 and tag 3 should be treated as seperate hashtags
#tag1 #tag2#tag3 

Hashtags inside URLs should be ignored

http://example.com/#urlpart

https://example.com#urlpart?s=search`;

while ((m = regex.exec(str)) !== null) {
  if (m[1] !== undefined) console.log(m[1]);
}

Related