iansinnott / react-string-replace

A simple way to safely do string replacement with React components
MIT License
652 stars 56 forks source link

Trouble formatting bold/italic/underline text with capture group regex #33

Open CaptainStiggz opened 6 years ago

CaptainStiggz commented 6 years ago

I'm having trouble using reactStringReplace to format text with bold/italic tags. I have it set up as follows:

var text1 = "[b]Testing bold [i]and italic[/i] tags[/b]"
var text2 = "[b]Testing bold [i]and italic tags[/b][/i]"

let replaced = reactStringReplace(text1, /\[b\]([\s\S]+)\[\/b\]/g, (match, i) => {
   return <b key={i}>{match}</b>
})
replaced = reactStringReplace(replaced, /\[i\]([\s\S]+)\[\/i\]/g, (match, i) => {
   return <i key={i}>{match}</i>
})

// result (html)
<b>Testing [i]bold and italic[/i] tags</b>
<b>Testing [i]bold and italic tags</b>[/i]

I'm not sure if this is a problem with reactStringReplace, with the regex I'm using, or something else. If I apply the italic replace first, I get italic tags where I'd expect them to be, but the [b] tags remain unchanged. Is this use case possible using reactStringReplace or do I need to use dangerouslySetInnerHtml?

Bonus: is reactStringReplace capable of handling unbalanced tags, or improperly formatted tags as in text2 or should I be doing some pre-processing to ensure the strings are properly balanced and formatted?

iansinnott commented 6 years ago

Hey @CaptainStiggz, yeah this lib does not handle nested replacements. The reason being, it's meant to be roughly equivalent to String.prototype.replace in it's functionality, however, it is true that we run into limitations since replacements can change the data type to something other than strings.

If you look here you'll see that it only runs replacements on strings. Once your first replacement has been run the element in the result array will be an object (i.e. <b>...</b>) so no replacements will be run on the inner string.

I'm open to suggestions if you have thoughts about how we might support more robust replacements.


To your second point, it just depends on the regex. If you can craft a regex that handles unbalanced/malformed tags then it should work fine. But there's no internal logic here to augment the functionality of the regex.

CaptainStiggz commented 6 years ago

Thanks @iansinnott! I've been fooling around with a recurisive augmentation as follows. It's still pretty rough, but it might be useful. This modification, combined with a slight modification to the regex I originally suggested (/\[i\]([\s\S]+)\[\/i\]/g should be lazy: /\[i\]([\s\S]+?)\[\/i\]/g) seems to work pretty well.

const reactStringReplaceRecursive = (input, pattern, fn, key=0) => {

   const isEmpty = (item) => {
      if(!item) return true
      if(item.hasOwnProperty('props')) {
         return false
      } else {
         return (item.length) ? false : true
      }
   }

   if(!input) {
      return null
   } else if(typeof input === "string") {
      return reactStringReplace(input, pattern, fn)
   }

   var output = []
   for(var i=0; i<input.length; i++) {
      const item = input[i]
      if(item) {
         if(typeof item === "string") {
            const next = reactStringReplace(item, pattern, fn)
            if(!isEmpty(next)) output.push(next)
         } else if(typeof item === "object") {        
            if(item.hasOwnProperty('props') && item.props.hasOwnProperty('children')) {
               const next = reactStringReplaceRecursive(item.props.children, pattern, fn, key+1)         
               if(!isEmpty(next)) {
                  const props = Object.assign({key: "k"+key+"i"+i}, item.props)
                  output.push(React.createElement(
                     item.type,
                     props,
                     next
                  ))
               }
            } else {
               const next = reactStringReplaceRecursive(item, pattern, fn, key+1)
               if(!isEmpty(next)) output.push(next)
            }
         }
      }
   }

   return output
}

This is a decent start, but still runs into some troublesome edge cases. For example, depending on the ordering of the bold/italic tags, I might end up with something like this:

const text = "[i][b]this should be bold and italic[/b][/i]"
// const text = "[b][i]this should be bold and italic[/i][/b]" - this would work
let replaced = reactStringReplace(text, /\[b\]([\s\S]+?)\[\/b\]/g, (match, i) => {
   return <b key={i}>{match}</b>
})
replaced = reactStringReplace(replaced, /\[i\]([\s\S]+?)\[\/i\]/g, (match, i) => {
   return <i key={i}>{match}</i>
})

// replaced => [
// "[i]",
// <b>this should be bold and italic</b>,
// "[/i]"
// ]

The problem gets pretty tricky here. Not exactly sure how I want to handle such cases.

iansinnott commented 6 years ago

You might consider using the React.Children API for the recursion in this case, since it will handle children regardless of whether it's a single element or an array. You would just need to check if the current item being iterated over was a react element.

That being said, you're use case is essentially a new rich text markup, which might be beyond the scope of simple regex and string replacement. You might consider using a stack to help you track opening/closing brackets and build up a deeply nested data structure representing your markup. Or do something like DraftJS does and annotate each character in the string with metadata which you subsequently use to render the markup. In either case you'll probably end up having to revert to strings and dangerouslySetInnerHTML.

DraftJS character metadata: https://draftjs.org/docs/api-reference-character-metadata.html