WordPress.org

Make WordPress Core

Ticket #14481: shortcode.patch

File shortcode.patch, 28.4 KB (added by deadowl, 8 years ago)

shortcode patch

  • shortcodes.php

     
    11<?php
    2 /**
    3  * WordPress API for creating bbcode like tags or what WordPress calls
    4  * "shortcodes." The tag and attribute parsing or regular expression code is
    5  * based on the Textpattern tag parser.
    6  *
    7  * A few examples are below:
    8  *
    9  * [shortcode /]
    10  * [shortcode foo="bar" baz="bing" /]
    11  * [shortcode foo="bar"]content[/shortcode]
    12  *
    13  * Shortcode tags support attributes and enclosed content, but does not entirely
    14  * support inline shortcodes in other shortcodes. You will have to call the
    15  * shortcode parser in your function to account for that.
    16  *
    17  * {@internal
    18  * Please be aware that the above note was made during the beta of WordPress 2.6
    19  * and in the future may not be accurate. Please update the note when it is no
    20  * longer the case.}}
    21  *
    22  * To apply shortcode tags to content:
    23  *
    24  * <code>
    25  * $out = do_shortcode($content);
    26  * </code>
    27  *
    28  * @link http://codex.wordpress.org/Shortcode_API
    29  *
    30  * @package WordPress
    31  * @subpackage Shortcodes
    32  * @since 2.5
    33  */
     2/*
     3Author: Jacob Beauregard (jrbeaure at uvm dot edu)
     4*/
    345
     6/*
     7
     8From Test Cases (http://svn.automattic.com/wordpress-tests/wp-testcase/test_shortcode.php)
     9==========================================================================================
     10Shortcode Statuses (to implement, or not to implement?)
     11        enabled = the shortcode works as normal (default)
     12        strip = the shortcode will be parsed and removed.  e.g. '[shortcode foo="bar"]' produces ''.  '[shortcode]foo[/shortcode]' produces 'foo'.
     13        faux = the shortcode will be abbreviated.  e.g. '[shortcode foo="bar"]' products '[shortcode]'.  '[shortocde]foo[/shortcode]' produces '[shortcode]'
     14        disabled = the shortcode is not parsed at all.  e.g. '[shortcode foo="bar"]' products '[shortcode foo="bar"]'
     15
     16
     17Major Differences/Improvements:
     18===============================
     19I. Addressing http://codex.wordpress.org/Shortcode_API#Limitations
     20
     21        1. You can nest any tag at any depth regardless of ancestors (Ticket #10702)
     22
     23        2. Tag and attribute names may match /[A-Za-z][-A-Za-z0-9_:.]*//* (trialing /* because that comment ends),
     24           with case-insensitive interpretation
     25
     26        3. Interpretation doesn't get tripped up by things like hyphens
     27
     28II. Addressing Ticket #12760,
     29
     30        1. Changed from fix in #6518
     31           Reasoning: balancing double-square-brackets can have misleading results with nesting
     32
     33        2. Shortcodes escapable by using [[, ]]
     34
     35        3. ']]' is escaped "right to left", so '[shortcode]]]' would evaluate to 'result]'
     36
     37        4. '[[' is escaped left to right '[[[shortcode]]]' => '[result]'
     38
     39III. Enhancements
     40
     41        1. Only matches valid shortcode for registered hooks, everything else will appear as text
     42
     43        2. Can register multiple hooks to single shortcode, uses priority (default: 10)
     44
     45IV. Conflicting Design Changes
     46
     47        1. Quoted literals are escaped by entities rather than cslashes
     48
     49        2. Inline (self-closing) shortcodes get passed content to accomodate multiple callbacks
     50
     51        3. No equivalent to shortcode_parse_atts function
     52           (Not marked private in function reference, but not documented in shortcode API page)
     53
     54        4. Boolean attributes take the place of positional attributes
     55           [foo bar] gets attributes array('bar' => 'bar') instead of array('0' => 'bar')
     56
     57        5. Disallows attribute and tag names that don't match /[A-Za-z][-A-Za-z0-9_:.]*/
     58
     59//pardon me /*
     60
     61        6. Disallows unquoted attribute values, unless they match /[-A-Za-z0-9_:.]+/
     62
     63
     64VI. Addressing Tickets #9264, #10082 (see following, `Basic Interpretation Method`)
     65
     66
     67Basic Interpretation Method:
     68============================
     691. If an open tag is encountered, it is added to the stack
     70
     712. If a close tag is encountered and there is no matching open tag on the stack
     72   the close tag is ignored
     73
     743. If a close tag is encountered and there is a matching open tag on the stack
     75   all opened tags on the stack before the matched tag will be implicitly self-closed
     76
     774. If text or an inline tag is encountered, it will be evaluated to its parent's
     78   content immediately
     79
     805. If tags are not closed by the end of the interpretation, they will be implicitly
     81   self-closed
     82
     83
     84Issues:
     85=======
     861. Haven't written new unit tests to reflect new functionality added
     87
     882. Documentation is not as good (though I hope most of the code is self-explanatory)
     89
     903. Not 100% backwards compatible
     91
     92*/
     93
     94/* +++++ PUBLIC FUNCTIONS ++++++ */
     95
    3596/**
    36  * Container for storing shortcode tags and their hook to call for the shortcode
    37  *
    38  * @since 2.5
    39  * @name $shortcode_tags
    40  * @var array
    41  * @global array $shortcode_tags
    42  */
    43 $shortcode_tags = array();
     97 * add hook for shortcode tag
     98*/
     99function add_shortcode($name,$handler,$priority=10) {
     100        $name = strtolower($name);
     101        shortcode_register_set('priority',$name,$handler,$priority);
     102        shortcode_register_set('callback',$name,$priority,$handler,true);
     103}
    44104
    45105/**
    46  * Add hook for shortcode tag.
    47  *
    48  * There can only be one hook for each shortcode. Which means that if another
    49  * plugin has a similar shortcode, it will override yours or yours will override
    50  * theirs depending on which order the plugins are included and/or ran.
    51  *
    52  * Simplest example of a shortcode tag using the API:
    53  *
    54  * <code>
    55  * // [footag foo="bar"]
    56  * function footag_func($atts) {
    57  *      return "foo = {$atts[foo]}";
    58  * }
    59  * add_shortcode('footag', 'footag_func');
    60  * </code>
    61  *
    62  * Example with nice attribute defaults:
    63  *
    64  * <code>
    65  * // [bartag foo="bar"]
    66  * function bartag_func($atts) {
    67  *      extract(shortcode_atts(array(
    68  *              'foo' => 'no foo',
    69  *              'baz' => 'default baz',
    70  *      ), $atts));
    71  *
    72  *      return "foo = {$foo}";
    73  * }
    74  * add_shortcode('bartag', 'bartag_func');
    75  * </code>
    76  *
    77  * Example with enclosed content:
    78  *
    79  * <code>
    80  * // [baztag]content[/baztag]
    81  * function baztag_func($atts, $content='') {
    82  *      return "content = $content";
    83  * }
    84  * add_shortcode('baztag', 'baztag_func');
    85  * </code>
    86  *
    87  * @since 2.5
    88  * @uses $shortcode_tags
    89  *
    90  * @param string $tag Shortcode tag to be searched in post content.
    91  * @param callable $func Hook to run when shortcode is found.
    92  */
    93 function add_shortcode($tag, $func) {
    94         global $shortcode_tags;
     106 * remove hook for shortcode tag
     107*/
     108function remove_shortcode($name,$handler) {
     109        $name = strtolower($name);
     110        $priority = shortcode_register_get('priority',$name,$handler);
    95111
    96         if ( is_callable($func) )
    97                 $shortcode_tags[$tag] = $func;
     112        shortcode_register_unset('priority',$name,$handler);
     113        shortcode_register_unset('callback',$name,$priority,$handler);
    98114}
    99115
    100116/**
    101  * Removes hook for shortcode.
    102  *
    103  * @since 2.5
    104  * @uses $shortcode_tags
    105  *
    106  * @param string $tag shortcode tag to remove hook for.
    107  */
    108 function remove_shortcode($tag) {
    109         global $shortcode_tags;
     117 * check if handler is hooked to shortcode tag, returns priority
     118*/
     119function has_shortcode($name,$handler) {
     120        $name = strtolower($name);
     121        return shortcode_register_get('priority',$name,$handler);
     122}
    110123
    111         unset($shortcode_tags[$tag]);
     124/**
     125 * removes all shortcode hooks
     126*/
     127function remove_all_shortcodes($name) {
     128        $name = strtolower($name);
     129        shortcode_register_unset('priority',$name);
     130        shortcode_register_unset('callback',$name);
    112131}
    113132
    114133/**
    115  * Clear all shortcodes.
    116  *
    117  * This function is simple, it clears all of the shortcode tags by replacing the
    118  * shortcodes global by a empty array. This is actually a very efficient method
    119  * for removing all shortcodes.
    120  *
    121  * @since 2.5
    122  * @uses $shortcode_tags
    123  */
    124 function remove_all_shortcodes() {
    125         global $shortcode_tags;
     134 * retrieves all shortcode hooks associated with a tag
     135 * returns order FCFS by priority
     136*/
     137function get_shortcodes($name) {
     138        $name = strtolower($name);
     139        $priority_groups = shortcode_register_get('callback',$name);
     140        $callbacks = array();
     141        if (is_array($priority_groups)) {
     142                foreach ($priority_groups as $priority_group) {
     143                        $callbacks = array_merge($callbacks,$priority_group);
     144                }
     145        }
     146        return array_keys($callbacks);
     147}
    126148
    127         $shortcode_tags = array();
     149/**
     150 * executes the shortcode hooks for the shortcode tag with specified attributes
     151 * initial content is $content, but is actively modified by each handler
     152*/
     153function apply_shortcodes($attrs,$content,$name) {
     154        $name = strtolower($name);
     155        $callbacks = get_shortcodes($name);
     156
     157        if (is_array($callbacks)) {
     158                foreach ($callbacks as $callback) {
     159                        if (function_exists($callback)) {
     160                                $content = $callback($attrs,$content,$name);
     161                        }
     162                }
     163        }
     164
     165        return $content;
    128166}
    129167
    130168/**
    131  * Search content for shortcodes and filter shortcodes through their hooks.
    132  *
    133  * If there are no shortcode tags defined, then the content will be returned
    134  * without any filtering. This might cause issues when plugins are disabled but
    135  * the shortcode will still show up in the post or content.
    136  *
    137  * @since 2.5
    138  * @uses $shortcode_tags
    139  * @uses get_shortcode_regex() Gets the search pattern for searching shortcodes.
    140  *
    141  * @param string $content Content to search for shortcodes
    142  * @return string Content with shortcodes filtered out.
    143  */
    144 function do_shortcode($content) {
    145         global $shortcode_tags;
     169 * removes shortcodes with registered handlers from page
     170*/
     171function strip_shortcodes($content) {
     172        $sc_re = "/" . shortcode_re_combine(array('tag_inline','tag_open','tag_close')) . "/";
     173        $content = preg_replace($sc_re,'',$content);
     174        return $content;
     175}
    146176
    147         if (empty($shortcode_tags) || !is_array($shortcode_tags))
    148                 return $content;
     177/**
     178 * alias of array_merge, unless we want to special case positional attributes
     179*/
     180function shortcode_atts($defaults, $attrs) {
     181        return array_merge($defaults, $attrs);
     182}
    149183
    150         $pattern = get_shortcode_regex();
    151         return preg_replace_callback('/'.$pattern.'/s', 'do_shortcode_tag', $content);
     184/* run main eval function */
     185function do_shortcode($expr) {
     186        return shortcode_eval($expr);
    152187}
    153188
     189
     190
     191/* +++++++++++++ THE UNDERPINNINGS +++++++++++ */
     192
    154193/**
    155  * Retrieve the shortcode regular expression for searching.
    156  *
    157  * The regular expression combines the shortcode tags in the regular expression
    158  * in a regex class.
    159  *
    160  * The regular expresion contains 6 different sub matches to help with parsing.
    161  *
    162  * 1/6 - An extra [ or ] to allow for escaping shortcodes with double [[]]
    163  * 2 - The shortcode name
    164  * 3 - The shortcode argument list
    165  * 4 - The self closing /
    166  * 5 - The content of a shortcode when it wraps some content.
    167  *
    168  * @since 2.5
    169  * @uses $shortcode_tags
    170  *
    171  * @return string The shortcode search regular expression
    172  */
    173 function get_shortcode_regex() {
    174         global $shortcode_tags;
    175         $tagnames = array_keys($shortcode_tags);
    176         $tagregexp = join( '|', array_map('preg_quote', $tagnames) );
     194 * retrieves all shortcode tags that have at least one registered hook
     195*/
     196function shortcode_tags() {
     197        $tags = array_keys(shortcode_register_get('callback'));
     198        return $tags;
     199}
    177200
    178         // WARNING! Do not change this regex without changing do_shortcode_tag() and strip_shortcodes()
    179         return '(.?)\[('.$tagregexp.')\b(.*?)(?:(\/))?\](?:(.+?)\[\/\2\])?(.?)';
     201/**
     202 * keeps track of information shared between many shortcode functions
     203*/
     204//I'm at war with global variables, so I used a static variable instead.
     205function shortcode_register($assignment=null) {
     206        static $register = array();
     207        if ($assignment !== null) {
     208                $register = $assignment;
     209        }
     210        return $register;
    180211}
    181212
    182213/**
    183  * Regular Expression callable for do_shortcode() for calling shortcode hook.
    184  * @see get_shortcode_regex for details of the match array contents.
    185  *
    186  * @since 2.5
    187  * @access private
    188  * @uses $shortcode_tags
    189  *
    190  * @param array $m Regular expression match array
    191  * @return mixed False on failure.
    192  */
    193 function do_shortcode_tag( $m ) {
    194         global $shortcode_tags;
     214 * gets a value from the register based on attributes
     215*/
     216function shortcode_register_get() {
     217        $arguments = func_get_args();
     218        $register  = shortcode_register();
    195219
    196         // allow [[foo]] syntax for escaping a tag
    197         if ( $m[1] == '[' && $m[6] == ']' ) {
    198                 return substr($m[0], 1, -1);
     220        $value = $register;
     221        while (count($arguments) && $value != null) {
     222                $key = array_shift($arguments);
     223                if (isset($value[$key])) {
     224                        $value = $value[$key];
     225                } else {
     226                        $value = null;
     227                }
    199228        }
     229        return $value;
     230}
    200231
    201         $tag = $m[2];
    202         $attr = shortcode_parse_atts( $m[3] );
     232/**
     233 * sets a value in the register
     234*/
     235function shortcode_register_set() {
     236        $arguments = func_get_args();
     237        $register  = shortcode_register();
     238        $value     = array_pop($arguments);
    203239
    204         if ( isset( $m[5] ) ) {
    205                 // enclosing tag - extra parameter
    206                 return $m[1] . call_user_func( $shortcode_tags[$tag], $attr, $m[5], $tag ) . $m[6];
    207         } else {
    208                 // self-closing tag
    209                 return $m[1] . call_user_func( $shortcode_tags[$tag], $attr, NULL,  $tag ) . $m[6];
     240        $group =& $register;
     241        while (count($arguments)) {
     242                $key = array_shift($arguments);
     243                if (!is_array($group[$key])) {
     244                        $group[$key] = array();
     245                }
     246                $group =& $group[$key];
    210247        }
     248        $group = $value;
     249        shortcode_register($register);
    211250}
    212251
    213252/**
    214  * Retrieve all attributes from the shortcodes tag.
    215  *
    216  * The attributes list has the attribute name as the key and the value of the
    217  * attribute as the value in the key/value pair. This allows for easier
    218  * retrieval of the attributes, since all attributes have to be known.
    219  *
    220  * @since 2.5
    221  *
    222  * @param string $text
    223  * @return array List of attributes and their value.
    224  */
    225 function shortcode_parse_atts($text) {
    226         $atts = array();
    227         $pattern = '/(\w+)\s*=\s*"([^"]*)"(?:\s|$)|(\w+)\s*=\s*\'([^\']*)\'(?:\s|$)|(\w+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|(\S+)(?:\s|$)/';
    228         $text = preg_replace("/[\x{00a0}\x{200b}]+/u", " ", $text);
    229         if ( preg_match_all($pattern, $text, $match, PREG_SET_ORDER) ) {
    230                 foreach ($match as $m) {
    231                         if (!empty($m[1]))
    232                                 $atts[strtolower($m[1])] = stripcslashes($m[2]);
    233                         elseif (!empty($m[3]))
    234                                 $atts[strtolower($m[3])] = stripcslashes($m[4]);
    235                         elseif (!empty($m[5]))
    236                                 $atts[strtolower($m[5])] = stripcslashes($m[6]);
    237                         elseif (isset($m[7]) and strlen($m[7]))
    238                                 $atts[] = stripcslashes($m[7]);
    239                         elseif (isset($m[8]))
    240                                 $atts[] = stripcslashes($m[8]);
     253 * unsets/deletes a value from the register
     254*/
     255function shortcode_register_unset() {
     256        $arguments = func_get_args();
     257        $register = short_code_register();
     258        $group =& $register;
     259
     260        $exists = true;
     261        while (count($arguments) > 1 && !$exists) {
     262                $key = array_shift($arguments);
     263                if (!is_array($group[$key])) {
     264                        $exists = false;
     265                } else {
     266                        $group =& $group[$key];
    241267                }
    242         } else {
    243                 $atts = ltrim($text);
    244268        }
    245         return $atts;
     269        if ($exists) {
     270                $key = array_shift($arguments);
     271                unset($group[$key]);
     272        }
     273        shortcode_register($register);
    246274}
    247275
     276/*
     277 * types of shortcodes to be parsed
     278*/
     279function shortcode_types() {
     280        return array(
     281                'esc_lsqbr',
     282                'esc_rsqbr',
     283                'tag_inline',
     284                'tag_open',
     285                'tag_close',
     286                'attr',
     287                'literal',
     288                'entity_ref',
     289                'char_ref',
     290                'char_str',
     291                'name',
     292                'text'
     293        );
     294}
     295
     296/*
     297 * subtypes of a root (raw) expression
     298*/
     299function shortcode_sub_root() {
     300        return array('esc_rsqbr','esc_lsqbr','tag_inline','tag_open','tag_close','text');
     301}
     302
     303/*
     304 * subtypes of a tag_inline expression
     305*/
     306function shortcode_sub_tag_inline() {
     307        return array('attr','name');
     308}
     309
     310/*
     311 * i bet you get the point by now
     312*/
     313function shortcode_sub_tag_open() {
     314        return array('attr','name');
     315}
     316
     317function shortcode_sub_tag_close() {
     318        return array('name');
     319}
     320
     321function shortcode_sub_text() {
     322        return null;
     323}
     324
     325function shortcode_sub_name() {
     326        return null;
     327}
     328
     329function shortcode_sub_attr() {
     330        return array('literal','name');
     331}
     332
     333function shortcode_sub_literal() {
     334        return array('char_ref','entity_ref','char_str');
     335}
     336
     337function shortcode_sub_char_str() {
     338        return null;
     339}
     340
     341function shortcode_sub_char_ref() {
     342        return null;
     343}
     344
     345function shortcode_sub_entity_ref() {
     346        //formally it's a name, but it's easier to interpret it with the & and ;
     347        return null; //array('name');
     348}
     349
     350function shortcode_sub_esc_lsqbr() {
     351        return null;
     352}
     353
     354function shortcode_sub_esc_rsqbr() {
     355        return null;
     356}
     357
    248358/**
    249  * Combine user attributes with known attributes and fill in defaults when needed.
    250  *
    251  * The pairs should be considered to be all of the attributes which are
    252  * supported by the caller and given as a list. The returned attributes will
    253  * only contain the attributes in the $pairs list.
    254  *
    255  * If the $atts list has unsupported attributes, then they will be ignored and
    256  * removed from the final returned list.
    257  *
    258  * @since 2.5
    259  *
    260  * @param array $pairs Entire list of supported attributes and their defaults.
    261  * @param array $atts User defined attributes in shortcode tag.
    262  * @return array Combined and filtered attribute list.
    263  */
    264 function shortcode_atts($pairs, $atts) {
    265         $atts = (array)$atts;
    266         $out = array();
    267         foreach($pairs as $name => $default) {
    268                 if ( array_key_exists($name, $atts) )
    269                         $out[$name] = $atts[$name];
    270                 else
    271                         $out[$name] = $default;
     359 * returns subtypes of type specified by $type
     360*/
     361function shortcode_sub($type='root') {
     362        if (in_array($type,shortcode_types()) || $type == 'root') {
     363                $func = "shortcode_sub_{$type}";
     364                $subtypes = $func();
    272365        }
    273         return $out;
     366        return $subtypes;
    274367}
    275368
     369
     370// started writing backwards compatibility function
     371// but would really prefer not to support these
     372
     373function shortcode_re_compat_name($named=false) {
     374        $expr = '\w+';
     375        if ($named) {
     376                $expr = "(?P<compat_name>{$expr})";
     377        }
     378        return $expr;
     379}
     380
     381function shortcode_re_compat_literal($named=false) {
     382        $nq = shortcode_re_compat_literal_nq();
     383        $sq = shortcode_re_compat_literal_sq();
     384        $dq = shortcode_re_compat_literal_dq();
     385        $expr = "(?:{$nq}|{$sq}|{$dq})";
     386        if ($named) {
     387                $expr = "(?P<compat_literal>{$expr})";
     388        }
     389        return $expr;
     390}
     391
     392function shortcode_re_compat_literal_dq() {
     393        $expr = '"[^"]*"';
     394        return $expr;
     395}
     396
     397function shortcode_re_compat_literal_sq() {
     398        $expr = "'[^']*'";
     399        return $expr;
     400}
     401
     402function shortcode_re_compat_literal_nq() {
     403        $expr = '\S+?(?=\])';
     404        return $expr;
     405}
     406
     407function shortcode_re_compat_attr($named=false) {
     408        $name = shortcode_re_compat_name();
     409        $literal = shortcode_re_compat_literal();
     410        $expr = "(?:{$name}\\s*=)?\\s*{$literal}";
     411        if ($named) {
     412                $expr = "(?P<compat_attr>{$expr})";
     413        }
     414        return $expr;   
     415}
     416
    276417/**
    277  * Remove all shortcode tags from the given content.
    278  *
    279  * @since 2.5
    280  * @uses $shortcode_tags
    281  *
    282  * @param string $content Content to remove shortcode tags.
    283  * @return string Content without shortcode tags.
    284  */
    285 function strip_shortcodes( $content ) {
    286         global $shortcode_tags;
     418 * regular expression to match a valid name
     419*/
     420function shortcode_re_name($named=false) {
     421        $expr = '[A-Za-z][-A-Za-z0-9_:.]*';
     422        if ($named) {
     423                $expr = "(?P<name>{$expr})";
     424        }
     425        return $expr;
     426}
    287427
    288         if (empty($shortcode_tags) || !is_array($shortcode_tags))
    289                 return $content;
     428/**
     429 * regular expression to match a name that's registered as a shortcode
     430*/
     431function shortcode_re_registered_name($named=false) {
     432        $tags = array_map('preg_quote',shortcode_tags());
     433        $expr = join('|',$tags);
     434        $expr = "(?i:{$expr})";
     435        if ($named) {
     436                $expr = "(?P<name>{$expr})";
     437        }
     438        return $expr;
     439}
    290440
    291         $pattern = get_shortcode_regex();
     441/**
     442 * you probably get the point by now
     443*/
     444function shortcode_re_char_str($named=false) {
     445        $expr =  "[^%&]+";
     446        if ($named) {
     447                $expr = "(?P<char_str>{$expr})";
     448        }
     449        return $expr;
     450}
    292451
    293         return preg_replace('/'.$pattern.'/s', '$1$6', $content);
     452/**
     453 * single quote version for shortcode_re_char_str
     454*/
     455function shortcode_re_char_str_sq($named=false) {
     456        $expr =  "[^%&']+";
     457        if ($named) {
     458                $expr = "(?P<char_str>{$expr})";
     459        }
     460        return $expr;
    294461}
    295462
    296 add_filter('the_content', 'do_shortcode', 11); // AFTER wpautop()
     463/**
     464 * double quote version for shortcode_re_char_str
     465*/
     466function shortcode_re_char_str_dq($named=false) {
     467        $expr = '[^%&"]+';
     468        if ($named) {
     469                $expr = "(?P<char_str>{$expr})";
     470        }
     471        return $expr;
     472}
    297473
    298 ?>
    299  No newline at end of file
     474function shortcode_re_char_ref($named=false) {
     475        $expr = "(?:[0-9]+|x[0-9a-fA-F]+)";
     476        if ($named) {
     477                $expr = "(?P<char_ref>{$expr})";
     478        }
     479        $expr = "&#{$expr};";
     480        return $expr;
     481}
     482
     483function shortcode_re_entity_ref($named=false) {
     484        $name = shortcode_re_name();
     485
     486        $expr = "&{$name};";
     487        if ($named) {
     488                $expr = "(?P<entity_ref>{$expr})";
     489        }
     490        return $expr;
     491}
     492
     493function shortcode_re_literal($named=false) {
     494        $nq = shortcode_re_literal_nq();
     495        $sq = shortcode_re_literal_sq();
     496        $dq = shortcode_re_literal_dq();
     497        $expr = "(?:{$nq}|{$sq}|{$dq})";
     498        if ($named) {
     499                $expr = "(?P<literal>{$expr})";
     500        }
     501        return $expr;
     502}
     503
     504function shortcode_re_literal_nq($named=false) {
     505        $expr = "[-a-zA-Z0-9_:.]+";
     506        return $expr;
     507}
     508
     509function shortcode_re_literal_sq($named=false) {
     510        $char_str = shortcode_re_char_str_sq();
     511        $char_ref = shortcode_re_char_ref();
     512        $entity_ref = shortcode_re_entity_ref();
     513
     514        $expr = "'(?:{$char_str}|{$char_ref}|{$entity_ref})*'";
     515        return $expr;
     516}
     517
     518function shortcode_re_literal_dq($named=false) {
     519        $char_str = shortcode_re_char_str_dq();
     520        $char_ref = shortcode_re_char_ref();
     521        $entity_ref = shortcode_re_entity_ref();
     522
     523        $expr =  "\"(?:{$char_str}|{$char_ref}|{$entity_ref})*\"";
     524        return $expr;
     525}
     526
     527function shortcode_re_attr($named=false) {
     528        $name  = shortcode_re_name();
     529        $literal = shortcode_re_literal();
     530
     531        $expr = "{$name}(?:={$literal})?";
     532        if ($named) {
     533                $expr = "(?P<attr>{$expr})";
     534        }
     535        return $expr;
     536}
     537
     538function shortcode_re_esc_lsqbr($named=false) {
     539        $expr = "\\[\\[";
     540        if ($named) {
     541                $expr = "(?P<esc_lsqbr>{$expr})";
     542        }
     543        return $expr;
     544}
     545
     546function shortcode_re_esc_rsqbr($named=false) {
     547        //two right square brackets not immediately followed by an odd number of right square brackets
     548        $expr = '\]\](?=(?:\]\])*(?!\]))';
     549        if ($named) {
     550                $expr = "(?P<esc_rsqbr>{$expr})";
     551        }
     552        return $expr;
     553}
     554
     555function shortcode_re_tag_inline($named=false) {
     556        $name = shortcode_re_registered_name();
     557        $attr = shortcode_re_attr();
     558
     559        $expr = "{$name}(?:\\s+{$attr})*";
     560        if ($named) {
     561                $expr = "(?P<tag_inline>{$expr})";
     562        }
     563        $expr = "\\[{$expr}\\s*\\/\\]";
     564        return $expr;
     565}
     566
     567function shortcode_re_tag_open($named=false) {
     568        $name = shortcode_re_registered_name();
     569        $attr = shortcode_re_attr();
     570
     571        $expr = "{$name}(?:\\s+{$attr})*";
     572        if ($named) {
     573                $expr = "(?P<tag_open>{$expr})";
     574        }
     575        $expr = "\\[{$expr}\\s*\\]";
     576        return $expr;
     577}
     578
     579function shortcode_re_tag_close($named=false) {
     580        $name = shortcode_re_registered_name();
     581        $expr = $name;
     582        if ($named) {
     583                $expr = "(?P<tag_close>{$expr})";
     584        }
     585        $expr = "\\[\\/{$expr}\\s*\\]";
     586        return $expr;
     587}
     588
     589function shortcode_re_text($named=false) {
     590        $expr = '(?s:.[^\[\]]*)';
     591        if ($named) {
     592                $expr = "(?P<text>{$expr})";
     593        }
     594        return $expr;
     595}
     596
     597/**
     598 * combines shortcode regex of types specified in parameters, returns delimited regex
     599*/
     600function shortcode_re_combine() {
     601        $types = func_get_args();
     602
     603        $regexps = array();
     604        foreach ($types as $type) {
     605                $re_func = "shortcode_re_$type";
     606                array_push($regexps,$re_func(1));
     607        }
     608        $statement = '/' . join('|',$regexps) . '/';
     609        return $statement;
     610}
     611
     612/**
     613 * returns regular expression to match subtypes of an expression (ex. attr => {$name}={$literal})
     614*/
     615function shortcode_re_subtypes($type) {
     616        $subtypes = shortcode_sub($type);
     617        $re = null;
     618        if (count($subtypes)) {
     619                $re = call_user_func_array('shortcode_re_combine',$subtypes);
     620        }
     621        return $re;
     622}
     623
     624/**
     625 * evaluates a name expression
     626*/
     627function shortcode_eval_name($name) {
     628        return strtolower($name['expression']);
     629}
     630
     631/**
     632 * evaluates a char_str expression
     633*/
     634function shortcode_eval_char_str($char_str) {
     635        return $char_str['expression'];
     636}
     637
     638/**
     639 * did you get the point yet?
     640*/
     641function shortcode_eval_char_ref($char_ref) {
     642        $expr = $char_ref['expression'];
     643        if ($expr[0] == 'x') {
     644                $expr = hexdec($expr);
     645        }
     646        $expr = chr($expr);
     647        return $expr;
     648}
     649
     650function shortcode_eval_entity_ref($entity_ref) {
     651        $expr = strtolower($entity_ref['expression']);
     652        $lookup = array_flip(get_html_translation_table(HTML_ENTITIES));
     653        return $lookup[$expr];
     654}
     655
     656function shortcode_eval_literal($literal) {
     657        $expr = "";
     658        foreach ($literal['children'] as $child) {
     659                $eval_func = "shortcode_eval_{$child['type']}";
     660                $expr .= $eval_func($child);
     661        }
     662        if ($expr[0] == "'" || $expr[0] == '"') {
     663                $expr = substr($expr,1,strlen($expr)-2);
     664        }
     665        return $expr;
     666}
     667
     668function shortcode_eval_attr($attr) {
     669        $children = $attr['children'];
     670        $key = shortcode_eval_name($children[0]);
     671        if (isset($children[1])) {
     672                $value = shortcode_eval_literal($children[1]);
     673        } else {
     674                $value = $key;
     675        }
     676        return array('key' => $key, 'value' => $value);
     677}
     678
     679function shortcode_eval_tag_inline($tag_inline,&$stack) {
     680        $children = $tag_inline['children'];
     681
     682        $name = shortcode_eval_name($children[0]);
     683        $attrs = array();
     684
     685        foreach ($children as $child) {
     686                if ($child['type'] == 'attr') {
     687                        $attr = shortcode_eval_attr($child);
     688                        $key = $attr['key'];
     689                        $value = $attr['value'];
     690                        $attrs[$key] = $value;
     691                }
     692        }
     693
     694        $content = apply_shortcodes($attrs,'',$name);
     695
     696        // adds evaluated content to next on the stack
     697        $node = array_pop($stack);
     698        $node['content'] .= $content;
     699        array_push($stack,$node);
     700
     701        return $node;
     702}
     703
     704function shortcode_eval_esc_lsqbr($esc_lsqbr,&$stack) {
     705        $node = array_pop($stack);
     706        $node['content'] .= "[";
     707        array_push($stack,$node);
     708
     709        return $node;
     710}
     711
     712function shortcode_eval_esc_rsqbr($esc_rsqbr,&$stack) {
     713        $node = array_pop($stack);
     714        $node['content'] .= "]";
     715        array_push($stack,$node);
     716
     717        return $node;   
     718}
     719
     720function shortcode_eval_tag_open($tag_open,&$stack,&$refs) {
     721        $children = $tag_open['children'];
     722
     723        $name = shortcode_eval_name($children[0]);
     724        $attrs = array();
     725
     726        foreach ($children as $child) {
     727                if ($child['type'] == 'attr') {
     728                        $attr = shortcode_eval_attr($child);
     729                        $key = $attr['key'];
     730                        $value = $attr['value'];
     731                        $attrs[$key] = $value;
     732                }
     733        }
     734
     735        $data = array('name' => $name, 'attrs' => $attrs);
     736        $node = array('node' => $data, 'content' => '');
     737
     738        //throws itself on the stack with attributes, etc.
     739        array_push($stack,$node);
     740        $refs[$name] += 1;
     741
     742        return $node;
     743}
     744
     745function shortcode_eval_tag_close($tag_close,&$stack,&$refs) {
     746        $value = null;
     747
     748        $name = shortcode_eval_name($tag_close['children'][0]);
     749
     750        if ($refs[$name] > 0) {
     751                do {
     752                        $child = array_pop($stack);
     753                        $child_node  = $child['node'];
     754                        $child_name  = $child['node']['name'];
     755                        $child_attrs = $child['node']['attrs'];
     756
     757                        $parent = array_pop($stack);
     758                        $parent['content'] .= apply_shortcodes($child_attrs,'',$child_name);
     759                        $parent['content'] .= $child['content'];
     760
     761                        $refs[$name] -= 1;
     762                        array_push($stack,$parent);
     763                } while ($child_name != $name);
     764        }
     765
     766        return array('name' => $name);
     767}
     768
     769function shortcode_eval_text($text,&$stack) {
     770        $node = array_pop($stack);
     771        $node['content'] .= $text['expression'];
     772        array_push($stack,$node);
     773        return $text['expression'];
     774}
     775
     776function shortcode_eval($expr) {
     777        $stack = array();
     778        $refs  = array();
     779
     780        //build parse tree
     781        $root = shortcode_build_tree($expr);
     782
     783        //create root content node, throw on stack
     784        $root_node = array('node' => 'root', 'content' => '');
     785        array_push($stack,$root_node);
     786
     787        //evaluate immediate children
     788        $subs = shortcode_sub();
     789        foreach ($root['children'] as $child) {
     790                if (in_array($child['type'],$subs)) {
     791                        $func = "shortcode_eval_{$child['type']}";
     792                        $func($child,$stack,$refs);
     793                }
     794        }
     795
     796        //evaluate children remaining on stack
     797        $child = array_pop($stack);
     798        while ($child['node'] != 'root') {
     799                $parent = array_pop($stack);
     800
     801                $child_node  = $child['node'];
     802                $child_name  = $child['node']['name'];
     803                $child_attrs = $child['node']['attrs'];
     804
     805                $parent['content'] .= apply_shortcodes($child_attrs,'',$child_name);
     806                $parent['content'] .= $child['content'];
     807                $child = $parent;
     808        }
     809
     810        return $child['content'];
     811}
     812
     813/**
     814* builds a tree from a shortcode expression
     815*/
     816function shortcode_build_tree($parent) {
     817        if (is_string($parent)) {
     818                $parent = array('type' => 'root', 'expression' => $parent);
     819        }
     820        $parent_type = $parent['type'];
     821        $parent_expression = $parent['expression'];
     822
     823        $re_subtypes = shortcode_re_subtypes($parent_type);
     824        $nodes = array();
     825        if ($re_subtypes) {
     826                $match_set = array();
     827                preg_match_all($re_subtypes,$parent_expression,$match_set,PREG_SET_ORDER);
     828
     829                foreach ($match_set as $index => $match) {
     830                        foreach (shortcode_types() as $type) {
     831                                if (strlen($match[$type]) > 0) {
     832                                        $nodes[$index] = array('type' => $type, 'expression' => $match[$type]);
     833                                }
     834                        }
     835                }
     836        }
     837
     838
     839        $parent['children'] = array_map('shortcode_build_tree',$nodes);
     840        return $parent;
     841}
     842
     843
     844add_filter('the_content','shortcode_eval',11);
     845 No newline at end of file