Disallow empty content in a BBCode tag with an attribute

Viewed 182

I have this regex:

\[code(?:=(["']?)(.{0,50}?)\1)?\](?!\s*\[\/code\])(.*?)\[\/code\]

This regex is supposed to support:

[code]content[/code]
[code=Title]content[/code]
[code="Title"]content[/code]
[code='Title']content[/code]

Empty contents [code][/code] are not allowed and this is done thanks to:

(?!\s*\[\/code\])

Also empty contents with title [code=Title][/code] are not allowed, and the above non-capturing group is working also for that condition, until I do not insert two tags together:

[code="title"][/code]
[code][/code]

How can I not make match the last condition by the regex?
The problem can best be observed here: https://regex101.com/r/J1dwJa/2/

As I understand, what it is creating problems is this part of the regex:

(["']?)

I am using the quantifier in order to support the pattern [code=Title][/code]. What this regex needs, at least I think, is that when it encounters ] should stop and do not go on. I am trying but I am not finding any path with my basic regex knowledge.

4 Answers

You should care about two things:

  1. . would match much more than needed

  2. You shouldn't match a [/code] in content part while looking for the closing [/code]

\[code(?>=(["']?)([^][]*)\1)?](?:(?!\s*(\[\/code])).)+(?3)

See live demo here

Regex engines will not give up on a match until it has tried all possibilities.
In your regex, this sequence .{0,50}? will match non-greedily, from 0 to 50 characters.

In your regex101 sample, you've specified the Dot-all modifier //s which means your dot there will span lines. It just so happens that on the next line it can satisfy a body (content) where (?! \s* [/code] ) passes.

You'll notice that for this to happen, the quote is given up on a backtrakck, so that ( ["']? ) never gets matched and the \1 is just an empty string. This leaves the door open for the non-greedy sequence to turn greedy. This is a GOTCHA moment.

Update

After further review, I firmly believe there is only 1 way
to satisfy all conditions to make this a viable regex.
The steps are:

  1. Wrap the quote part around an atomic group.
    This makes this part of the match impervious to backtracking.

  2. Use an alternation to distinguish between a quoted value or non-quoted.
    Put the quoted part first.
    Note that you can't use a negative class inside the quoted part at all (i.e. [^\[\]] ) since the idea of a quote is to allow delimiters like [].
    This part has to passively allow any character ..
    This will allow the matching of a string like this
    [code="t][/code]"]hello world[/code]
    however implausible generating such a string may be
    .

And that's it. If it's done any other way it's just plain wrong.
I include a link that shows all the possible cases.
If you find an error... ah, there is none.

Group 2 and 3 contain the values (one or the other), just join
them.
Group 4 contains the content.

(?s)\[code(?>(?:=(?:(["'])(.{0,50}?)\1|([^\]]{0,50})))?)\](?!\s*\[\/code\])(.*?)\[\/code\]

https://regex101.com/r/cO73iA/1

Explained

 (?s)                      # Dot-all modifier
 \[code                    # Open bbcode tag
 (?>                       # Atomic group, can't be backtracked into
      (?:
           =
           (?:
                ( ["'] )                  # (1), Quote
                ( .{0,50}? )              # (2), code value
                \1                        # Backref to Quote
             |                          # or,
                ( [^\]]{0,50} )           # (3), Un-quoted code value
           )
      )?
 )
 \]
 (?! \s* \[/code\] )       # Cannot be empty content
 ( .*? )                   # (4), Content, must be some
 \[/code\]                 # Close bbcode tag

you can use the following updated regex

\[code(?:=(["']?)([^'"]{0,50}?)\1)?\](?!\s*\[\/code\])(.*?)\[\/code\]

instead of use .{0,50} use [^'"]{0,50} that match any char except " or '

check its matches in the following link

My guess is that your expression works fine, we might just want to remove the s flag, if we wish to these instance in the original demo:

[code='title'][/code]
[code="title"][/code]

Demo 1

We might also be able to a bit simplify our expression to:

\[code(?:=(["']?)(.{0,50}?)\1)?\](.+?)\[\/code\]

Demo 2

Related