Nested shortcode functions accessing variable

After a bit of lateral thinking and lots of testing I was able to come up with a solution that negated the need for scope crossing variables, or using a global variable for that matter.

The code goes like this:

  1. Split the nested $content(nested shortcodes) into single shortcodes with preg_match_all
  2. Apply do_shortcode to each shortcode with a for loop
  3. Each nested shortcode function returns a string which will be split into 2 strings by another preg_match function, with the 1st being the main content data and the 2nd being the nav data
  4. Assign all the main content data to one array and the nav data to another
  5. Then it’s just a matter of processing the two arrays and returning the string!

I’ve supplied a slimmed down version of my code below. The trick for all this to work is having a robust preg_match expression to separate the shortcodes, so that they won’t break when fed irregular user data.

<?php
add_shortcode('tabs', 'tabs_func');
//Main shortcode function
function tabs_func( $atts, $content = null ) {  
    $a = shortcode_atts( array(
        'id' => '',
    ), $atts );

    //array to store nav data
    $navCon = array();

    //array to store main content data
    $tabCon = array();

    /* 
    Preg_match_all splits $content into predefined individual shortcodes [imgTab], [vidTab] and [htmlTab][/htmlTab]

    A basic explanation of the expression I used. If you want a more detailed breakdown, plug the expression below into "https://regexr.com/"

        (?<!\[)    <= checks if there's a square bracket behind the proceeding match, to avoid matching escaped shortcode
        \[    <= matches the opening bracket
        (?:(?:imgTab|vidTab).*(?<!\])\](?!\])|htmlTab[\S\s]*?\[\/htmlTab\](?!\]))    <= can't break this into parts easily, but in essence this part tries to match a string which next part goes 'imgTab', 'vidTab', or 'htmlTab' (although the 'htmlTab' is put in it's own non-capture group as it has to deal with possible linebreaks). It then matches the remaining characters until it gets to a closing square bracket (or in the case of htmlTab, '[/htmlTab]. It then checks that there aren't two closing brackets next to each other before finishing the match. 
    */
    preg_match_all( '@(?<!\[)\[(?:(?:imgTab|vidTab).*(?<!\])\](?!\])|htmlTab[\S\s]*?\[\/htmlTab\](?!\]))@', $content, $matches );   

    $matchLI = 1;
    //loops through each of the shortcode matches
    foreach ($matches[0] as $match) {

        //runs current shortcode and assigns to variable
        $match_processed = do_shortcode($match);

        /*
        This expression is much more simple than the last one :D

            ([\S\s]+)    <= this capture group basically takes all the characters that aren't matched by the next part of the expression. This capture group contains the main content
            (<[\S\s]+>)    <= this capture group matches a 'less-than' character, a string of any characters and finally a 'greater-than' character. Since this expression is greedy it won't false match any 'greater-than' symbols in the first capture group, as it will look for the last 'greater-than' in the string
        */
        preg_match( '@([\S\s]+)(<[\S\s]{4,}>)@', $match_processed, $navMatch );

        //assigns nav data of current shortcode to the $navCon array, using the $matchLI value to index it
        $navCon[$matchLI] .= "<li class="nav_NUM_" . $matchLI . "">" . $navMatch[2] . "</li>";

        //assigns main content data of current shortcode to the $tabCon array, using the $matchLI value to index it
        $tabCon[$matchLI] = "<section class="content_NUM_" . $matchLI . "">" . $navMatch[1] . "</section>";
        }

        //increments the value for the next loop
        $matchLI++;

    }

    //constructing html in $tabReturn variable
    $tabReturn = "<div id='" . $a['id'] . "'>";
    $tabReturn .= "<nav><ul>";
    //loops through and assigns content of $navCon array to $tabReturn
    foreach ($navCon as $navElement) {
        $tabReturn .= $navElement;
    }
    $tabReturn .= "</ul></nav>";
    //loops through and assigns content of $tabCon array to $tabReturn
    foreach ($tabCon as $tabElement) {
        $tabReturn .= $tabElement;
    }
    $tabReturn .= "</div>";

    //finished html string is returned. Mission complete!
    return $tabReturn;

}

add_shortcode('imgTab', 'imgTab_func');
function imgTab_func( $atts ) { 
    $a = shortcode_atts( array(
        'src' => '',
        'width' => '100%',
        'height' => 'auto'
    ), $atts );

    //First img element is the main content, and the second is the nav preview 
    return "<img src="" . $a["src'] . "' width="" . $a["width'] . "' height="" . $a["height'] . "'>
    <img src="". $a["src'] . "' width="" . $a["width'] . "' height="" . $a["height'] . "'>";


}

add_shortcode('vidTab', 'vidTab_func');
function vidTab_func( $atts ) { 
    $a = shortcode_atts( array(
        'poster' => '',
        'src' => '',
        'width' => '100%',
        'height' => 'auto'
    ), $atts );

    //vid element is the main content, and the img is the nav preview 
    return "<video width="" . $a["width'] . "' height="" . $a["height'] . "' poster="" . $a["poster'] . "' controls>
        <source src="" . $a["src'] . "' type="video/mp4">
    </video>
    <img src="". $a["poster'] . "' width="" . $a["width'] . "' height="" . $a["height'] . "'>";

}

add_shortcode('htmlTab', 'htmlTab_func');
function htmlTab_func( $atts, $content = null ) {   
    $a = shortcode_atts( array(
        'poster' => '',
        'width' => '100%',
        'height' => 'auto'
    ), $atts );

    //$content is the main content, and the img is the nav preview.
    return $content . "<img src="". $a["poster'] . "' width="" . $a["width'] . "' height="" . $a["height'] . "'>";
}

And the shortcode that the user would type into the text editor:

[tabs id="test1"]

//any number and combination of these elements

[imgTab src="https://wordpress.stackexchange.com/questions/295847/imgTab.jpg" width="100" height="100"]

[vidTab poster="vidTab.jpg" src="vidTab.mp4" width="128" height="72"]

[htmlTab poster="htmlTab.jpg" width="100" height="100"] 
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam scelerisque maximus neque gravida gravida.</p>
[/htmlTab]

[/tabs]

And the output should be something like this:

<div id='test1'>
    <nav><ul>
        <li class="nav_NUM_1"> 
            <img src="https://wordpress.stackexchange.com/questions/295847/imgTab.jpg" width="100" height="100">
        </li>
        <li class="nav_NUM_2"> 
            <img src="vidTab.jpg" width="128" height="72">
        </li>
        <li class="nav_NUM_3"> 
            <img src="htmlTab.jpg" width="100" height="100">
        </li>
    </ul></nav>
    <section class="content_NUM_1">
        <img src="https://wordpress.stackexchange.com/questions/295847/imgTab.jpg" width="100" height="100">
    </section>
    <section class="content_NUM_2">
        <video width="128" height="72" poster="vidTab.jpg" controls>
            <source src="vidTab.mp4" type="video/mp4">
        </video>
    </section>
    <section class="content_NUM_3">
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam scelerisque maximus neque gravida gravida.</p>
    </section>
</div>

Ready to be processed by a javascript tab function!