How can I replace the Nth instance of a string in PHP?

I need to dynamically add an active class to a shortcode in wordpress based on the active number set in it’s parent shortcode.

The parent looks like this:

Read More
$active = 3;
$output .= "ntt".wpb_js_remove_wpautop($content);

The $content variable can technically consist of anything in between the shortcodes tags, but specifically, should include more child shortcodes (it’s a tabs shortcode so the parent is tabs and the children are tab).

What I need to do is parse $content and replace the Nth ($active variable) instance of [vc_tab with [vc_tab active="true"

I realize there are better ways to design the shortcode to compensate for this but I am limited in what I can do because I am modifying Visual Composer to use Bootstrap tabs instead of jQuery and Bootstrap tabs need the active class on both the <li> and the <div class="tab-pane"> elements but I don’t want the users to have to add an active number to the parent shortcode and an active true/false to the child as that will get confusing.

So far from googling I can only find replacing the first occurrence or all occurrences, not the Nth one.

Temporarily I am using jQuery to make the appropriate <div> active but it results in an undesirable FOUC where there is no pane visible on load and then one pops into place when the jQuery runs.

$('.tabs-wrap').each(function(){
  var activeTab = $(this).find('.tabbed li.active a').attr('href');
  $(this).find(activeTab).addClass('active');
});  

Related posts

Leave a Reply

2 comments

  1. This is an interesting problem, and got me thinking if you really need a regex to do this.

    My conclusion is that I don’t believe you do. All you need to do is find the occurrence of the Nth $needle, and replace it with your replacement. This can be achieved through some simple string manipulation, as follows:

    1. Define some variables. Your input string, your $active index, the $needle that we are looking for, and the $replacement that we will replace the Nth match with.

      $string  = "[vc_tab [vc_tab [vc_tab [vc_tab [vc_tab [vc_tab";
      $active  = 3;
      $needle  = "[vc_tab";
      $replace = '[vc_tab active="true"';
      
    2. Now, we need to make sure that there are enough $needles in the $string, otherwise we can’t do this replacement.

      if( substr_count( $string, $needle) < $active) {
          throw new Exception("There aren't enough needles in the string to do this replacement");
      }
      
    3. Now, we know we can do the replacement. So let’s find the $index in the string where the Nth occurrence of $needle ends:

      $count = 0; $index = 0;
      while( $count++ < $active) {
          $index = strpos( $string, $needle, $index) + strlen( $needle);
      }
      
    4. Now, $index is pointing here in the string:

      [vc_tab [vc_tab [vc_tab [vc_tab [vc_tab [vc_tab
                             ^
                             Here
      

      We can now do some simple substr()‘s to form the final string:

      $result = substr( $string, 0, $index - strlen( $needle)) . $replace . substr( $string, $index);
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^              ^^^^^^^^^^^^^^^^^^^^^^^^
                Before the Nth match                                        After the Nth match
      

      So, we end up concatenating:

      • [vc_tab [vc_tab
      • [vc_tab active="true"
      • [vc_tab [vc_tab [vc_tab

    This results in our final output:

    [vc_tab [vc_tab [vc_tab active="true" [vc_tab [vc_tab [vc_tab
    

    Try it out in the demo!

  2. I ‘m not familiar with the tools you mention so I can’t say if there is a better way to achieve the end goal, but you can replace the Nth occurrence of something using preg_replace_callback:

    $active = 3;     // target occurrence
    $occurrence = 0; // counter
    $output = preg_replace_callback(
        '/[vc_tab/',
        function ($matches) use(&$occurrence, $active) {
            return ++$occurrence != $active
                ? $matches[0]
                : '[vc_tab active="true"';
        },
        $output,
        $active
    );
    

    See it in action.

    This works by replacing each occurrence with itself unless the occurrence counter is equal to the target number.

    Note that I have passed $active as the maximum number of occurrences to replace in order to get free performance increase; this will cause preg_replace_callback to stop after $active occurrences have been replaced (we know that all occurrences after that will be replaced by themselves, so there’s no point in going on).