I think I may have thrown you slightly when I mentioned lazy quantifiers. The truth is, using greedy quantifiers improves pattern efficiency.
Regarding your atomic grouping, I don't see any benefit (in my pattern anyhow) because I am actively avoiding the need to backtrack in each "alternative" (the expressions between the pipes).
For the single and double quote and multiline comment alternatives, I want the dot to match "any character AND new lines". For the inline comment alternative, I want the dot to match "any non-newline character". For this reason, I am using "inline modifiers", specifically (?s)
where needed.
I try to use pattern delimiting characters that do not occur in my pattern. This helps to make the pattern more readable and prevents having to do unnecessary escaping.
~ # pattern delimiter
(?| # branch reset
(")(?:[^"\\]|\\(?s).)*" # capture double quote, match any non-double-quote/non-backslash unless backslash is followed by any character (used as an escaping character)
| # OR
(')(?:[^'\\]|\\(?s).)*' # capture single quote, match any non-single-quote/non-backslash unless backslash is followed by any character (used as an escaping character)
| # OR
(#|//).* # capture hash or two-slashes, match the rest of the line # OR
(/\*)(?s).*?\*/ # capture \*, match zero or more of any character (including newlines), then */ (lazy quantifier)
| # OR
(<!--)(?s).*?-->) # capture <!--, match zero or more of any character (including newlines) (lazy quantifier)
) # end branch reset
~ # pattern delimiter
(Pattern Demo)
Implementation: (PHP Demo)
const PATTERN = <<<'PATTERN'
~(?|(")(?:[^"\\]|\\(?s).)*"|(')(?:[^'\\]|\\(?s).)*'|(#|//).*|(/\*)(?s).*?\*/|(<!--)(?s).*?-->)~
PATTERN;
const LOOKUP = [
'#' => 'gainsboro',
'//' => 'lightgrey',
'/*' => 'silver',
'<!--' => 'darkgrey',
"'" => 'mint',
'"' => 'aqua'
];
echo preg_replace_callback(PATTERN, function($m) {
return "<span style=\"color:" . LOOKUP[$m[1]] . ";\">{$m[0]}</span>";
}, $string);
By declaring the lookup array as a constant, scoping issues are avoided inside of the preg_replace_callback()
callback function. In other words, you don't have to pass in the lookup array with use()
.
I am using nowdoc syntax when declaring the pattern constant so that I don't need to escape any quotes. I am declaring a constant for no other reason than the fact that the pattern will not change in the script.
By wrapping the whole expression in a "branch reset" ((?|...)
), you can avoid calling end($m)
inside the custom function to access the captured group. The branch reset ensures that each capture group is always the second element in the matches output (at [1]
). If you removed the branch reset in the Regex101 demo above, you will see that the captured "marker" matches will have differing indexes.
Hmm... If your code is relying on the different indexes as part of a lookup-based replacement, then don't use the branch reset -- it's a good chance to break out one of the new weapons: array_key_last($m).
Test Input:
$string = <<<'STRING'
//single line comment
random text ... #another comment
Multiline comments:
/* this is a multiline comment
with 'squote and "dquote"
matches the whole thing */
// single line 'squoted' "dquoted" comment w/ extra " for no reason
More comments <!-- yatta yatta
yatta
yatta -->
Quotes:
"also matches strings with \" escaped quotes or 'the other kind of quotation marks in it' "
a "nested 'squote with nested \"dquote\"'" assuming only outermost quoting matters for formatting
'matches the end quote because it it not escaped \\'
STRING;
Output (unrendered):
<span style="color:lightgrey;">//single line comment</span>
random text ... <span style="color:gainsboro;">#another comment</span>
Multiline comments:
<span style="color:silver;">/* this is a multiline comment
with 'squote and "dquote"
matches the whole thing */</span>
<span style="color:lightgrey;">// single line 'squoted' "dquoted" comment w/ extra " for no reason</span>
More comments <span style="color:darkgrey;"><!-- yatta yatta
yatta
yatta --></span>
Quotes:
<span style="color:aqua;">"also matches strings with \" escaped quotes or 'the other kind of quotation marks in it' "</span>
a <span style="color:aqua;">"nested 'squote with nested \"dquote\"'"</span> assuming only outermost quoting matters for formatting
<span style="color:mint;">'matches the end quote because it it not escaped \\'</span>
Reasons to re-invent the wheel. The wheel that you need is very narrow and employing/loading a complete library may be overkill in terms of performance or memory consumption for your task.
In terms of step count, my pattern is slightly more efficient than yours. Though to be honest, I have been informed (by Stackoverflow regex gurus who I respect) that step count is not a reliable metric to gauge pattern efficiency. I generally use it, though, as a rough indicator of pattern efficiency.
I reckon taking these opportunities to sharpen regular expression knowledge is a healthy exercise for programmers. The more you work with regular expressions, the less scary they become.
I am purposely not bothering to acknowledge any tinfoil-hat fringe cases regarding premature-terminating substrings like:
/* arithmetic symbols include: +-*/ */
and
<!-- This is a long --> arrow -->
because these deliberate monkeywrenches are not commonly escaped by slashes and they can be sensibly overcome by recrafting the comment. Such as:
/* re-ordered arithmetic symbols include: +-/* */
and
<!-- I only use short -> arrows in comments-->