Reputation: 10026
I have a string which looks like
var std = new Bammer({mode:"deg"}).bam(0, 112).bam(177.58, (line-4)/2).bam(0, -42)
.ramBam(8.1, 0).bam(8.1, (slot_height-thick)/2)
I want to put a tag around the last .bam()
or .ramBam()
.
str.replace(/(\.(ram)?bam\(.*?\))$/i, '<span class="focus">$1</span>');
And I hope to get:
new Bammer({mode:"deg"}).bam(0, 112).bam(177.58, (line-4)/2).bam(0, -42).ramBam(8.1, 0)<span class="focus">.bam(8.1, (slot_height-thick)/2)</span>
But somehow I keep on fighting with the non greedy parameter, it wraps everything after new Bammer with the span tags. Also tried a questionmark after before the $
to make the group non greedy.
I was hoping to do this easy, and with the bam
or ramBam
I thought that regex would be the easiest solution but I think I'm wrong.
Where do I go wrong?
Upvotes: 0
Views: 111
Reputation: 110755
You can use the following regex:
(?!.*\)\.)(\.(?:bam|ramBam)\(.*\))$
(?!.*\)\.) # do not match ').' later in the string
( # begin capture group 1
.\ # match '.'
(?:bam|ramBam) # match 'bam' or 'ramBam' in non-cap group
\(.*\) # match '(', 0+ chars, ')'
) # end capture group 1
$ # match end of line
For the example given in the question the negative lookahead (?!.*\)\.)
moves an internal pointer to just before the substring:
.bam(8.1, (slot_height-thick)/2)
as that is the first location where there is no substring ).
later in the string.
If there were no end-of-line anchor $
and the string ended:
...0).bam(8.1, (slot_height-thick)/2)abc
then the substitution would still be made, resulting in a string that ends:
...0)<span class="focus">.bam(8.1, (slot_height-thick)/2)</span>abc
Including the end-of-line anchor prevents the substitution if the string does not end with the contents of the intended capture group.
Upvotes: 2
Reputation: 44313
Regex to use:
/\.((?:ram)?[bB]am\([^)]*\))(?!.*\.(ram)?[bB]am\()/
\.
Matches period.(?:ram)?
Optionally matches ram
in a non-capturing group.[bB]am
Matches bam
or Bam
.\(
Matches (
.[^)]*
Matches 0 or more characters as long as they are not a )
.)
Matches a )
. Items 2. through 6. are placed in Capture Group 1.(?!.*\.(ram)?[bB]am\()
This is a negative lookahead assertion stating that the rest of the string contains no further instance of .ram(
or .rambam(
or .ramBam(
and therefore this is the last instance.let str = 'var std = new Bammer({mode:"deg"}).bam(0, 112).bam(177.58, 0).bam(0, -42).ramBam(8.1, 0).bam(8.1, slot_height)';
console.log(str.replace(/\.((?:ram)?[bB]am\([^)]*\))(?!.*\.(ram)?[bB]am\()/, '<span class="focus">.$1</span>'));
Update
The JavaScript regular expression engine is not powerful enough to handle nested parentheses. The only way I know of solving this is if we can make the assumption that after the final call to bam
or ramBam
there are no more extraneous right parentheses in the string. Then where I had been scanning the parenthesized expression with \([^)]*\)
, which would fail to pick up final parentheses, we must now use \(.*\)
to scan everything until the final parentheses. At least I know no other way. But that also means that the way that I had been using to determine the final instance of ram
or ramBam
by using a negative lookahead needs a slight adjustment. I need to make sure that I have the final instance of ram
or ramBam
before I start doing any greedy matches:
(\.(?:bam|ramBam)(?!.*\.(bam|ramBam)\()\((.*)\))
\.
Matches .
.(?:bam|ramBam)
Matches bam or ramBam.(?!.*\.(bam|ramBam)\()
Asserts that Item 1. was the final instance\(
Matches (
.(.*)
Greedily matches everything until ...\)
the final )
.)
Items 1. through 6. are placed in Capture Group 1.let str = 'var std = new Bammer({mode:"deg"}).bam(0, 112).bam(177.58, (line-4)/2).bam(0, -42) .ramBam(8.1, 0).bam(8.1, (slot_height-thick)/2)';
console.log(str.replace(/(\.(?:bam|ramBam)(?!.*\.(bam|ramBam)\()\((.*)\))/, '<span class="focus">$1</span>'));
Upvotes: 2
Reputation: 1779
The non-greedy flag isn't quite right here, as that will just make the regex select the minimal number of characters to fit the pattern. I'd suggest that you do something with a negative lookahead like this:
str.replace(/(\.(?:ram)?[Bb]am\([^)]*\)(?!.*(ram)?[Bb]am))/i, '<span class="focus">$1</span>');
Note that this will only replace the last function name (bam
OR ramBam
), but not both. You'd need to take a slightly different approach to be able to replace both of them.
Upvotes: 1