0

I have a string "hello @steph the email you requested is test@test.com for user @test"

I want to turn this into:

['hello ', <a href="">@steph</a>, 'the email you requested is test@test.com for user ', <a href="">@test</a>];

here is what i have:

function matchUserMention(text) {
    var pattern = /\B@[a-z0-9_-]+/gi;
    return text.match(pattern); // [@steph, @test]
}

function applyUser(string) {
    let text = string;
    if (typeof text != 'string') return text;
    var arr = [];

    function replaceAll(str, find, replace) {
        return text.replace(new RegExp(find, 'g'), replace);
    }
    const matches = matchUserMention(text);
    _.each(matches, (match) => {
        text = replaceAll(text, match, <a href={'https://test.co'}>${match}</a>);
    });
    return text; 
}

text right now returns:

'hello <a href="">@steph</a>, the email you requested is test@test.com for user <a href="">@test</a>

Does anyone know the best way of creating the array from here? It cant be to heavey as I need this to be called all the time

ReganPerkins
  • 1,645
  • 3
  • 17
  • 36

4 Answers4

1

You could use split with a regular expression that has a capture group. This will result in an array that has the matching elements at the odd indexes. You can then apply the <a> wrap to those via .map(). Finally, you can use .filter() to remove the empty strings (e.g. at the start or end of the array):

// Sample data
var url = 'https://elk.co';
var string = "hello @steph the email you requested is test@test.com for user @test";

// The transformation
var parts = string.split(/(\B@[\w-]+)/g)
                  .map( (w,i) => i%2 ? `<a href='${url}'>${w}</a>` : w )
                  .filter(Boolean);

// Output the result
console.log(parts);
trincot
  • 317,000
  • 35
  • 244
  • 286
  • Be mindful of using template literals if browser compatibility with older IE versions (and possibly others) is necessary: http://stackoverflow.com/a/28088965/1236397 – James Wilkins Jan 10 '17 at 22:04
  • OK, @JamesWilkins, thanks for highlighting that. I have replaced `&$` with `$1` since the capture group was there anyway. For what concerns the template literal: the OP seemed to already use one, although the syntax used in the question is not entirely correct. – trincot Jan 10 '17 at 22:13
  • I may be wrong on the `&$` part. I usually use $1, but I see articles on it since 2011. :/ – James Wilkins Jan 10 '17 at 22:15
  • Can't seem to find out if `$&` is widely supported for str.replace(), etc. - "This feature is non-standard and is not on a standards track. Do not use it on production sites facing the Web" (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastMatch) may only be applicable to the property on the global RegExp object itself. `$1` works just as well though. – James Wilkins Jan 10 '17 at 22:20
  • 1
    Indeed, that reference is a bout the `$&` property of the `RegExp` object, not the `$&` backreference. The latter is used without reservation in the [Regular Expressions topic on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) – trincot Jan 10 '17 at 22:28
1
function applyUser(string) {
  const reg = /\B@[a-z0-9_-]+/ig;
  const res = [];
  let match;

  // loop while we can match something
  while((match = reg.exec(string)) !== null) {
    // push to result array portion of string before user match, if any
    if (match.index > 0) {
      res.push(string.substring(0, match.index));
    }

    // push updated user name to result array
    res.push('<a href="">' + match[0] + '</a>');

    // remove processed part from a string
    string = string.substr(match.index + match[0].length);
  }

  // push any reminder to the result array
  if (string) {
    res.push(string);
  }
  return res;
}
  • Thanks for the comment, but I wrote it in `ES6`, imo the same standard author of the question used, given she uses `let` and `const` – Marian Javorka Jan 11 '17 at 09:04
0

This approach matches the usernames and uses the RegExp capability of remembering the last match postion. So subsequent exec() call will match subsequent usernames. The match object returned by exec() is an array containing the matched texts and an index attribute pointing to the match position.

The code copies the text from the last position (initially position 0) to the matched username, then appends the "wrapped" username and starts over until there are no more username matches.

The code is commented line by line, describing its function:

function wrapUsernames(str) {
        // match "usernames" ("at" followed by text, preceeded by non-word character or line-start), 
        // the RegExp keeps its state
    var re = /(?:([^\w]|^)(@[a-z]+))/g,
        // store a RegExp match object
        match,
        // store the text fragments
        results = [],
        // remember last index for substring copy
        lastIndex = 0;

    // match and store result, returns null if it does no longer match
    while (match = re.exec(str)) {
        // copy text from last match / start to username 
        // (only if not matched at index 0 to prevent empty string)
        if (match.index != 0) {
            results.push( str.substring(lastIndex, match.index) );
        }
        if (match[1].length >= 1) {
            // it also matched the char before the username, append it
            results.push(match[1]);
        }
        // copy matched username and wrap in a tag
        results.push('<a href="...">' + match[2] + '</a>');
        // update the index to start copy at the next position
        lastIndex = match.index + match[0].length;
    }

    // append the remaining string (only if it wouldn't be an empty string)
    if (lastIndex < str.length) {
        results.push(str.substring(lastIndex));
    }

    return results;
}

This should also match usernames prefixed with other characters than space:

> wrapUsernames("(@steph) the email you requested is test@test.com for user (@test)")
< Array [ "(", "<a href="...">@steph</a>", ") the email you requested is test@test.com for user ", "(", "<a href="...">@test</a>", ")" ]
> wrapUsernames("@steph the email you requested is test@test.com for user (@test)")
< Array [ "<a href="...">@steph</a>", " the email you requested is test@test.com for user ", "(", "<a href="...">@test</a>", ")" ]
> wrapUsernames("hi, @steph the email you requested is test@test.com for user @test")
< Array [ "hi,", " ", "<a href="...">@steph</a>", " the email you requested is test@test.com for user ", " ", "<a href="...">@test</a>" ]
try-catch-finally
  • 7,436
  • 6
  • 46
  • 67
0

Thanks everyone, this ended up being what i used in case anyone else happens across this. It is a bit of a mix of the other responses.

 function applyUser(str) {
        var arr = [],
            userRegEx = /\B@[a-z0-9_-]+/gi,
            execResult,
            lastFoundLen = 0,
            found,
            strLen = str.length,
            index = 0;
        while ((execResult = userRegEx.exec(str)) !== null) {
            const newIndex = execResult.index;
            if (newIndex > index + lastFoundLen) arr.push(str.substring(index + lastFoundLen, newIndex));
            found = execResult[0];
            if (!found) break;
            const foundLen = found.length;
            const userId = execResult[0].substring(1);
            arr.push(
                <ProfilePersona
                    key={userId}
                    noAvatar
                    view="compact"
                    userId={userId}>
                    {userId}
                </ProfilePersona>
            );

            index = newIndex;
            lastFoundLen = foundLen;
        }
        if (index + lastFoundLen < strLen) arr.push(str.substr(index + lastFoundLen));
        if (!arr.length) arr.push(str);
        return arr;
    }
ReganPerkins
  • 1,645
  • 3
  • 17
  • 36