阐述 `do_shortcode()` 函数的源码,它是如何解析和执行短代码(Shortcode)的?

各位代码爱好者,大家好!我是你们今天的短代码解剖师,准备好了吗?咱们要深入 do_shortcode() 这个WordPress世界里的小小却强大的函数,看看它如何像一位精明的厨师,将那些看似简单的短代码标签,烹饪成美味的功能大餐。

第一幕:短代码的舞台

想象一下,短代码就像舞台上的演员,它们在文章或页面内容中占据着位置,等待着被激活,然后执行特定的任务。do_shortcode() 就是那个舞台监督,它负责寻找这些演员,并指导他们完成表演。

首先,我们需要明确什么是短代码。简单来说,短代码就是用方括号包裹起来的标签,例如 [contact-form]。这些标签可以接受参数,例如 。WordPress允许开发者注册自己的短代码,并将它们与特定的PHP函数关联起来。当 do_shortcode() 遇到一个短代码时,它会调用与之关联的函数,并将短代码的属性传递给这个函数。

第二幕:do_shortcode() 的源代码剖析

让我们来看看 do_shortcode() 的源代码(基于 WordPress 6.4.3,为了简化讲解,我省略了一些不常用的功能和注释):

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 );
}

这段代码看起来很简单,但它却隐藏着很多细节。让我们一步一步地拆解它:

  1. 检查短代码标签是否存在:

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

    这段代码首先声明 $shortcode_tags 为全局变量。$shortcode_tags 是一个关联数组,它存储了所有已注册的短代码标签及其对应的处理函数。如果这个数组为空或不是数组,那么说明没有任何短代码被注册,函数直接返回原始的内容,不做任何处理。这就像如果舞台上没有演员,舞台监督就直接回家了。

  2. 构建正则表达式:

    $pattern = get_shortcode_regex();

    get_shortcode_regex() 函数负责生成一个正则表达式,用于匹配短代码标签。这个正则表达式非常复杂,但它的作用是识别各种形式的短代码,包括带属性的短代码、自闭合短代码等等。我们稍后会详细分析 get_shortcode_regex() 的工作原理。

  3. 执行替换:

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

    这行代码是 do_shortcode() 的核心。它使用 preg_replace_callback() 函数,将所有匹配到的短代码标签替换为它们经过处理后的内容。preg_replace_callback() 函数接受三个参数:

    • "/$pattern/s": 这是用于匹配短代码的正则表达式。s 修饰符表示点号 (.) 可以匹配换行符,这对于处理包含换行符的短代码内容非常重要。
    • 'do_shortcode_tag': 这是一个回调函数,当正则表达式匹配到一个短代码时,preg_replace_callback() 会调用这个函数来处理匹配到的短代码。
    • $content: 这是要进行短代码处理的原始内容。

    preg_replace_callback() 会在 $content 中查找所有匹配 $pattern 的短代码,然后对每一个匹配到的短代码,调用 do_shortcode_tag() 函数进行处理,最后将 $content 中所有匹配到的短代码替换为 do_shortcode_tag() 函数返回的结果。

第三幕:get_shortcode_regex() 的正则表达式魔法

get_shortcode_regex() 函数是构建用于匹配短代码的正则表达式的关键。 让我们看一下简化后的代码:

function get_shortcode_regex( $tagnames = null ) {
    global $shortcode_tags;

    if ( empty( $tagnames ) ) {
        $tagnames = array_keys( $shortcode_tags );
    }
    $tagregexp = join( '|', array_map( 'preg_quote', $tagnames ) );

    // WARNING! Do not change this regex without changing do_shortcode_tag() and the shortcode parsing.
    return
        '\['                              // Opening bracket
        . '(\[?)'                           // 1: Optional second opening bracket for escaping shortcodes: [[tag]]
        . "($tagregexp)"                     // 2: Shortcode name
        . '(?![\w-])'                       // Word boundary
        . '('                                  // 3: Unroll the loop: Inside the opening shortcode tag
        .     '[^\]\/]*'                   // Not a closing bracket or forward slash
        .     '(?:'
        .         '\/(?!\])'               // Not 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]]
}

这段代码的作用是根据已注册的短代码标签,生成一个正则表达式。这个正则表达式可以匹配各种形式的短代码,包括:

  • [tag]:简单的短代码
  • [tag attribute="value"]:带有属性的短代码
  • [tag]content[/tag]:带有内容的短代码
  • [tag /]:自闭合短代码
  • [[tag]]:被转义的短代码(不会被解析)

让我们来分解一下这个正则表达式:

  • \[: 匹配一个左方括号。
  • (\[?): 匹配一个可选的左方括号。用于处理转义的短代码,例如 [[tag]]
  • ($tagregexp): 匹配短代码的标签名。$tagregexp 是一个由所有已注册的短代码标签组成的正则表达式,例如 (gallery|contact-form)
  • (?![\w-]): 确保短代码标签名后面不是字母、数字或短划线。这是一个词语边界的判断。
  • ([^\]\/]*(?:\/(?!\])[^\]\/]*)*?): 匹配短代码的属性。
  • (?:(\/)\]|\](?:([^\[]*(?:\[(?!\/\2\])[^\[]*)*)\[\/\2\])?): 匹配短代码的闭合标签。
  • (\]?): 匹配一个可选的右方括号。用于处理转义的短代码,例如 [[tag]]

这个正则表达式非常复杂,但它的作用是准确地识别各种形式的短代码。理解这个正则表达式对于理解 do_shortcode() 的工作原理至关重要。

第四幕:do_shortcode_tag() 的幕后工作

do_shortcode_tag() 函数是真正执行短代码逻辑的地方。当 preg_replace_callback() 找到一个匹配的短代码时,它会调用 do_shortcode_tag() 函数,并将匹配到的短代码信息传递给它。

function do_shortcode_tag( $matches ) {
    global $shortcode_tags;

    // Allow [[foo]] syntax for escaping a tag.
    if ( $matches[1] == '[' && $matches[6] == ']' ) {
        return substr( $matches[0], 1, -1 );
    }

    $tag = $matches[2];
    $attr = shortcode_parse_atts( $matches[3] );

    if ( empty( $shortcode_tags[ $tag ] ) ) {
        return $matches[0];
    }

    return call_user_func( $shortcode_tags[ $tag ], $attr, $matches[5], $tag );
}

让我们一步一步地分析这段代码:

  1. 处理转义的短代码:

    if ( $matches[1] == '[' && $matches[6] == ']' ) {
        return substr( $matches[0], 1, -1 );
    }

    这段代码检查是否是转义的短代码,例如 [[tag]]。如果是,它会移除外层的方括号,并返回转义后的内容。

  2. 提取短代码标签和属性:

    $tag = $matches[2];
    $attr = shortcode_parse_atts( $matches[3] );

    这段代码从 $matches 数组中提取短代码的标签名和属性。$matches 数组是 preg_replace_callback() 函数传递给 do_shortcode_tag() 函数的,它包含了正则表达式匹配到的所有信息。$matches[2] 包含了短代码的标签名,$matches[3] 包含了短代码的属性字符串。shortcode_parse_atts() 函数负责解析属性字符串,将其转换为一个关联数组。

  3. 检查短代码是否已注册:

    if ( empty( $shortcode_tags[ $tag ] ) ) {
        return $matches[0];
    }

    这段代码检查短代码的标签名是否在 $shortcode_tags 数组中存在。如果不存在,说明这个短代码没有被注册,函数直接返回原始的短代码标签,不做任何处理。

  4. 调用短代码的处理函数:

    return call_user_func( $shortcode_tags[ $tag ], $attr, $matches[5], $tag );

    这行代码是 do_shortcode_tag() 的核心。它使用 call_user_func() 函数调用与短代码标签关联的处理函数。$shortcode_tags[ $tag ] 包含了处理函数的名称。call_user_func() 函数接受一个回调函数和一组参数,它会调用这个回调函数,并将参数传递给它。

    传递给处理函数的参数包括:

    • $attr: 短代码的属性数组。
    • $matches[5]: 短代码的内容(如果存在)。
    • $tag: 短代码的标签名。

    处理函数会根据这些参数,执行特定的逻辑,并返回处理后的内容。do_shortcode_tag() 函数会将这个内容返回给 preg_replace_callback() 函数,然后 preg_replace_callback() 函数会将原始的短代码标签替换为处理后的内容。

第五幕:shortcode_parse_atts() 的属性解析

shortcode_parse_atts() 函数负责将短代码的属性字符串解析为一个关联数组。例如,对于短代码 shortcode_parse_atts() 函数会将属性字符串 ids="1,2,3" columns="4" 解析为以下数组:

array(
    'ids' => '1,2,3',
    'columns' => '4'
)

让我们看一下 shortcode_parse_atts() 的源代码:

function shortcode_parse_atts( $text ) {
    $atts = array();
    $pattern = '/(w+)s*=s*"([^"]*)"(?:s|$)|(w+)s*=s*'([^']*)'(?:s|$)|(w+)s*=s*([^s'"]+)(?:s|$)|"([^"]*)"(?:s|$)|('([^']*)')(?:s|$)|(w+)(?:s|$)/';

    $text = trim( $text );
    if ( empty( $text ) ) {
        return $atts;
    }

    preg_match_all( $pattern, $text, $match, PREG_SET_ORDER );
    if ( empty( $match ) ) {
        return $atts;
    }

    foreach ( $match as $m ) {
        if ( ! empty( $m[1] ) ) {
            $atts[ strtolower( $m[1] ) ] = stripcslashes( $m[2] );
        } elseif ( ! empty( $m[3] ) ) {
            $atts[ strtolower( $m[3] ) ] = stripcslashes( $m[4] );
        } elseif ( ! empty( $m[5] ) ) {
            $atts[ strtolower( $m[5] ) ] = stripcslashes( $m[6] );
        } elseif ( isset( $m[7] ) && $m[7] !== '' ) {
            $atts[] = stripcslashes( $m[7] );
        } elseif ( isset( $m[9] ) ) {
            $atts[] = stripcslashes( $m[9] );
        } elseif ( isset( $m[10] ) ) {
            $atts[] = strtolower( $m[10] );
        }
    }

    return $atts;
}

这段代码使用正则表达式来匹配各种形式的属性,包括:

  • attribute="value"
  • attribute='value'
  • attribute=value
  • "value"
  • 'value'
  • attribute

preg_match_all() 函数会将所有匹配到的属性信息存储在 $match 数组中。然后,代码遍历 $match 数组,将属性名和属性值添加到 $atts 数组中。stripcslashes() 函数用于移除属性值中的反斜杠。

第六幕:一个完整的例子

让我们通过一个完整的例子来演示 do_shortcode() 的工作原理。

假设我们定义了一个名为 [greeting] 的短代码,它的作用是显示一个问候语。我们使用以下代码注册这个短代码:

function greeting_shortcode( $atts, $content = null ) {
    $atts = shortcode_atts(
        array(
            'name' => 'World',
        ),
        $atts,
        'greeting'
    );

    return 'Hello, ' . esc_html( $atts['name'] ) . '!';
}
add_shortcode( 'greeting', 'greeting_shortcode' );

这段代码首先定义了一个名为 greeting_shortcode() 的函数,这个函数是 [greeting] 短代码的处理函数。然后,它使用 add_shortcode() 函数将 [greeting] 短代码与 greeting_shortcode() 函数关联起来。

greeting_shortcode() 函数接受两个参数:

  • $atts: 短代码的属性数组。
  • $content: 短代码的内容(如果存在)。

在这个例子中,我们使用了 shortcode_atts() 函数来定义短代码的默认属性。shortcode_atts() 函数接受三个参数:

  • $defaults: 默认属性数组。
  • $atts: 短代码的属性数组。
  • $shortcode: 短代码的标签名。

shortcode_atts() 函数会将 $atts 数组与 $defaults 数组合并,并返回合并后的数组。如果 $atts 数组中没有包含 $defaults 数组中的属性,那么 $defaults 数组中的属性将被添加到 $atts 数组中。

在这个例子中,我们定义了一个名为 name 的默认属性,它的值为 World。这意味着,如果我们在使用 [greeting] 短代码时没有指定 name 属性,那么 name 属性的值将默认为 World

greeting_shortcode() 函数的最后一行代码使用 esc_html() 函数来转义 name 属性的值,以防止 XSS 攻击。然后,它返回一个包含问候语的字符串。

现在,假设我们在文章中使用以下短代码:

[greeting name="John"]

当 WordPress 解析这篇文章时,它会调用 do_shortcode() 函数来处理短代码。do_shortcode() 函数会找到 [greeting name="John"] 短代码,并调用 greeting_shortcode() 函数。

greeting_shortcode() 函数会接收到以下参数:

$atts = array(
    'name' => 'John'
);
$content = null;

greeting_shortcode() 函数会使用 shortcode_atts() 函数将 $atts 数组与默认属性数组合并。由于 $atts 数组中已经包含了 name 属性,因此 shortcode_atts() 函数不会修改 $atts 数组。

然后,greeting_shortcode() 函数会使用 esc_html() 函数来转义 name 属性的值,并将转义后的值插入到问候语字符串中。最后,greeting_shortcode() 函数会返回以下字符串:

Hello, John!

do_shortcode() 函数会将原始的短代码 [greeting name="John"] 替换为 Hello, John!,并在文章中显示这个问候语。

第七幕:总结与彩蛋

do_shortcode() 函数是 WordPress 中一个非常重要的函数,它负责解析和执行短代码。理解 do_shortcode() 函数的工作原理对于开发 WordPress 插件和主题至关重要。

希望通过今天的讲解,大家对 do_shortcode() 函数有了更深入的了解。记住,短代码就像舞台上的演员,do_shortcode() 就像舞台监督,它们一起为我们呈现精彩的功能表演。下次遇到复杂的短代码问题时,不妨回到这篇文章,重新梳理一下思路,相信你一定能够找到解决方案。

彩蛋:

  • remove_all_shortcodes() 函数可以移除所有已注册的短代码。
  • strip_shortcodes() 函数可以移除字符串中的所有短代码标签,但不执行它们。
  • 短代码可以嵌套使用,但需要注意避免无限循环。

感谢大家的聆听,希望今天的讲座对您有所帮助!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注