Is it possible to nest wordpress shortcodes that are the same shortcode?
Asked Answered
S

4

6

I know it is possible to nest shortcodes if they use the do_shortcode wrapper, however, the codex states:

"However the parser will fail if a shortcode macro is used to enclose another macro of the same name:"

Is there a way around this?

for example if I have the a shortcode to make a div such as:

[div]some content in a div[/div]

I want to be able to use:

[div]
    [div]a nested div[/div]
[/div]

but this will fail with the standard do_shortcode wrapper.

My temporary workaround is the duplicate the shortcode with _parent appended to the name but than I can only nest 1 level deep unless I created div_parent1, div_parent2 etc...

Spears answered 1/11, 2012 at 19:23 Comment(5)
What you look for is documented in the wordpress documentation: codex.wordpress.org/Shortcode_API#Nested_ShortcodesBusterbustle
@Busterbustle can you point me to which line in there shows it - from what I can see it only says what I quoted in the question that it is not possible to nest shortcodes of the same name due to a limitation in the context-free regexp parser.Spears
Well, read: "The shortcode parser correctly deals with nested shortcode macros, provided their handler functions support it by recursively calling do_shortcode()" - just do that. call do_shortcode() in your own shortcode hook. That's how it is done in Wordpress. -- That is the first line of the section "Nested Shortcodes" by the way.Busterbustle
Hmm, right, misread you. The issue is known however, it has been reported about two years ago: core.trac.wordpress.org/ticket/14481Busterbustle
@Busterbustle thanks for the input - for this particular shortcode 1 level of nesting should be enough so I'm just going to stick with a duplicated shortcode with _parent appended. That should save me some processing resources and dev time.Spears
M
3

If you are writing the shortcode there is a simple solution. You can write several shortcodes that call the same function. I have shortcodes for creating html blocks such as divs, and have several with names like div, block1, block2 eg.

add_shortcode('div', 'devondev_block');
add_shortcode('block', 'devondev_block');
add_shortcode('block2', 'devondev_block');

They all call the same function. They can be nested as long as you remember to use different short codes.

The WordPress short code support suffers from the attempt to do the parsing with regex only. It is possible to do this sort of parsing with a mixture of regex, a finite state machine, and a stack. That approach can handle nesting and can be very fast, especially when there are very few shortcodes. Every time I encounter this I'm tempted to give it a try.

Mothy answered 7/1, 2013 at 20:38 Comment(1)
Thanks - as mentioned in the question this is what I ended up sticking with but with '_parent' appended to the shortcode for one of them as I only need to nest 1 deep.Spears
N
1

The API tells it like it is, thus this is not possible:

This is a limitation of the context-free regexp parser used by do_shortcode() - it is very fast but does not count levels of nesting, so it can't match each opening tag with its correct closing tag in these cases.

The functions in question in the latest version (3.4.2) are:

function do_shortcode($content) {
    global $shortcode_tags;

    if (empty($shortcode_tags) || !is_array($shortcode_tags))
        return $content;

    $pattern = get_shortcode_regex();
    return preg_replace_callback( "/$pattern/s", 'do_shortcode_tag', $content );
}

function get_shortcode_regex() {
    global $shortcode_tags;
    $tagnames = array_keys($shortcode_tags);
    $tagregexp = join( '|', array_map('preg_quote', $tagnames) );

    // WARNING! Do not change this regex without changing do_shortcode_tag() and strip_shortcode_tag()
    return
          '\\['                              // Opening bracket
        . '(\\[?)'                           // 1: Optional second opening bracket for escaping shortcodes: [[tag]]
        . "($tagregexp)"                     // 2: Shortcode name
        . '\\b'                              // Word boundary
        . '('                                // 3: Unroll the loop: Inside the opening shortcode tag
        .     '[^\\]\\/]*'                   // Not a closing bracket or forward slash
        .     '(?:'
        .         '\\/(?!\\])'               // A forward slash not followed by a closing bracket
        .         '[^\\]\\/]*'               // Not a closing bracket or forward slash
        .     ')*?'
        . ')'
        . '(?:'
        .     '(\\/)'                        // 4: Self closing tag ...
        .     '\\]'                          // ... and closing bracket
        . '|'
        .     '\\]'                          // Closing bracket
        .     '(?:'
        .         '('                        // 5: Unroll the loop: Optionally, anything between the opening and closing shortcode tags
        .             '[^\\[]*+'             // Not an opening bracket
        .             '(?:'
        .                 '\\[(?!\\/\\2\\])' // An opening bracket not followed by the closing shortcode tag
        .                 '[^\\[]*+'         // Not an opening bracket
        .             ')*+'
        .         ')'
        .         '\\[\\/\\2\\]'             // Closing shortcode tag
        .     ')?'
        . ')'
        . '(\\]?)';                          // 6: Optional second closing brocket for escaping shortcodes: [[tag]]
}
Nevanevada answered 1/11, 2012 at 19:29 Comment(5)
Yes I see that. I'm asking if there is a way around it - ie not using the do_shortcode but maybe some similar function that doesn't use the context free regexp parser but rather a parser that does count levels of nestingSpears
In theory you could do that, but you would have to not use the standard shortcode functionality and attach it to the the_content filter or something similar. Probably best to not use the [shortcode] syntax to avoid conflicts with Wordpress.Nevanevada
You would also be sacrificing performance, parsing the content for nested patterns would be expensive.Nevanevada
Look at wp-incudes/shortcodes.php - you will need to duplicate this functionality and write your own regex parser as there are no hooks for modifying this behavior.Nevanevada
thanks for the input - for this particular shortcode 1 level of nesting should be enough so I'm just going to stick with a duplicated shortcode with _parent appended. That should save me some processing resources and dev time.Spears
T
1

As explained before Wordpress still doesn't support nested shortcodes of the same type.

So you need to duplicate your shortcodes for nested instances.
However you may try this approach based on some regex preprocessing added via 'the_content' hook. .

  • before parsing/converting shortcodes we automatically add numeric suffixes like [div_1][/div_1]
  • we duplicate shortcodes in a loop according to a maximum nesting level

Preprocess content for numeric suffixes


 function counter_suffix_to_nested_shortcodes($content, $nested_shortcodes = [])
 {
     // Define the regular expression pattern for the shortcodes
     $pattern = '/(\[\/?[a-zA-Z0-9\-\_]+(?: [^\]]+)?(?:_\d+)?\])/is';
 
     // Define a function to handle the replacements
     $callback = function ($matches) use (&$suffixStack, &$nested_shortcodes) {
 
         // get tag name
         $tag = $matches[0];
         $pattern = '/\[\/?([A-Za-z0-9\-\_]+)/';
         preg_match($pattern, $tag, $tagNameMatches);
         $tag_name = $tagNameMatches[1];
         $last_tag='';
         //$suffixStack=[];
         $suffixCounter = 0;
 
         // not in array of shortcode type: return shortcode as it is
         if (!in_array($tag_name, $nested_shortcodes)) {
             return $tag;
         }
 
         // Extract the shortcode name 
         preg_match($pattern, $tag, $tagNameMatches);
         $shortcode_name = $tagNameMatches[1];
 
 
         // Check if it's a closing tag
         if (strpos($tag, '/' . $tag_name) !== false) {
             $suffix = array_pop($suffixStack);
             // Ensure the suffix is correctly placed in the closing tag
             if ($suffix > 0) {
                 $tag = str_replace(['[/', ']'], ['[/', '_' . $suffix . ']'], $tag);
                 $last_tag = "[$tag_name]";
             }
         } else {
             // Only increment suffixCounter if it's not previous tag and suffixStack is not empty/reset
             if ( !empty($suffixStack) && $tag_name != $last_tag) {
                 $suffixCounter = count($suffixStack);
             } else {
                 //reset counter
                 $suffixCounter = 0;
                 $suffixStack = [];
             }
 
             $suffixStack[] = $suffixCounter;
 
             // get new shortcode retaining all attributes
             $suffix = $suffixCounter > 0 ? '_' . $suffixCounter : '';
             $tag = str_replace('[' . $shortcode_name, '[' . $shortcode_name . $suffix, $tag);
 
         }
 
         return $tag;
     };
 
     // Initialize the suffix stack and counter
     $suffixStack = [];
 
     // Perform the replacement using the callback function
     $content = preg_replace_callback($pattern, $callback, $content);
 
     return $content;
 }

  // pereprocess content: don't wrap shortcodes
  function sanitize_nested_shortcodes($content)
  {
      global $nested_shortcodes;
  
      // unwrap shortcodes in p tags
      $replace = [
          '<p>[' => '[',
          ']</p>' => ']',
          '</p>[' => '[',
          ']<br />' => ']',
          '&#8220;' => '"',
          '&#8217;' => '"',
          '&#8216;' => '"',
          '&#8243;' => '"'
      ];
      // add index suffixes to nested shortcodes
      $content = counter_suffix_to_nested_shortcodes(strtr($content, $replace), $nested_shortcodes);
  
      return $content;
  }
  add_filter('the_content', 'sanitize_nested_shortcodes'); }
 }

Define shortcode

/**
 * div_generate shortcode
 * add class names and IDs
 */
function div_generate($atts, $content = null)
{
    extract(shortcode_atts(
        [
            'class' => '',
            'id' => ''
        ], $atts));

    $cnt_filtered =
        '<div id="' . $id . '" class="' . $class . '">' .
        do_shortcode($content) .
        '</div>';

    return $cnt_filtered;
}

Define shortcode

/**
 * div_generate shortcode
 * add class names and IDs
 */
function div_generate($atts, $content = null)
{
    extract(shortcode_atts(
        [
            'class' => '',
            'id' => ''
        ], $atts));

    $cnt_filtered =
        '<div id="' . $id . '" class="' . $class . '">' .
        do_shortcode($content) .
        '</div>';

    return $cnt_filtered;
}

Duplicate shortcodes

/**
 * define nested shortcode 
 * names/identifieers: 
 * this prevents to break existing shortcodes
 * to allow 20 nesting levels 
 */

 $nested_shortcodes = ['div'];
 $nesting_max = 20;
 
 // duplicate shortcodes
 foreach($nested_shortcodes as $shortcode){
     for ($i = 0; $i < $nesting_max; $i++) {
         $suffix = $i === 0 ? '' : '_' . $i;
         add_shortcode( $shortcode.$suffix, $shortcode.'_generate');
     }
 }

Now we can easily add nested shortcodes in WP editor without manually adding suffixes like so:

[div class="outer"]
    [div class="nested-level1"]
        <p>Content</p>
        [div class="nested-level2"]
            <p>Content</p>
        [/div]
    [/div]
[/div]

Output (before parsing)

[div class="outer"]
    [div_1 class="nested-level1"]
        <p>Content</p>
        [div_2 class="nested-level2"]
            <p>Content</p>
        [/div_2]
    [/div_1]
[/div]

Output (parsed)

<div id="" class="outer">
  <div id="" class="nested-level1">
    <p>Content</p>
    <div id="" class="nested-level2">
      <p>Content</p>
    </div>
  </div>
</div>
Treatise answered 26/7 at 19:8 Comment(0)
B
0

You need to do the shortcodes again for the content in your shortcode. Example:

add_shortcode('div', function($attributes, $content, $tag) {
    ...
    do_shortcode($content);
    ...
});

See as well: What is the best way to enable nested shortcodes? (Wordpress SE)

Just seeing: This does not solve the problem in Wordpress so does not answer your question. I thought it would have been fixed that until now.

Busterbustle answered 1/11, 2012 at 19:37 Comment(9)
This doesn't work with the same nested shortcode, per the docs.Nevanevada
Oh they are not yet using recursion even PCRE supports that? Hmm, well, then this indeed needs a bug report with Wordpress and is a limitation of the software. Patching get_shortcode_regex should fix this. Let me take a look if a patch exists.Busterbustle
However the parser will fail if a shortcode macro is used to enclose another macro of the same name - it's a performance thing supposedly.Nevanevada
@doublesharp: Maybe. The regex does not look that well in any case, so actually performance wise this could be improved anyway.Busterbustle
Agreed, but it is what it is for now, although the many will love you if you offered a better patch for this regex :)Nevanevada
I think the problem here is that they did not follow the route they are doing with balance tags for HTML. It would be equally easy to do it that way and would allow much more.Busterbustle
@doublesharp: There is a suggestion in trac, give it some traction if you like to see some changes with the current status quo.Busterbustle
@doublesharp: Please read this: kore-nordmann.de/blog/parse_with_regexp.html , it's probably easy to change the regex in the function with what is outlined there.Busterbustle
The other problem with the shortcode syntax is that like html <p> a close isn't always required. So it is very hard to support nesting using a regex even with recursion.Mothy

© 2022 - 2024 — McMap. All rights reserved.