剖析 WordPress `WP_Block_Parser` 类的源码:如何将文章内容中的区块字符串解析为结构化对象。

各位观众老爷,大家好!今天咱们来聊聊 WordPress 的一个重要组成部分——WP_Block_Parser,也就是区块解析器。这玩意儿就像一个细心的侦探,专门负责把文章内容里那些看似乱七八糟的区块字符串,抽丝剥茧,还原成结构清晰的对象,方便 WordPress 后续进行处理和展示。

咱们的目标很明确:深入理解 WP_Block_Parser 的工作原理,看看它是如何把一堆字符串变成可用的数据的。准备好了吗?Let’s dive in!

一、区块字符串的本质:HTML 注释中的 JSON

首先,我们需要搞清楚 WordPress 区块的存储方式。它不是什么神秘的代码,本质上就是藏在 HTML 注释里的 JSON 数据。例如:

<!-- wp:paragraph -->
<p>Hello, world!</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":123,"sizeSlug":"large"} -->
<figure class="wp-block-image size-large"><img src="image.jpg" alt="" class="wp-image-123"/></figure>
<!-- /wp:image -->

可以看到,每个区块都由一个开始注释 <!-- wp:block-name {attributes} --> 和一个结束注释 <!-- /wp:block-name --> 包裹。中间的部分是区块的内容,而开始注释中的 {attributes} 部分则是区块的属性,以 JSON 格式存储。

WP_Block_Parser 的任务就是找到这些注释,提取区块名称和属性,然后把它们组合成一个易于处理的数据结构。

二、WP_Block_Parser 类:核心代码剖析

WP_Block_Parser 类的核心方法是 parse(),它接收一个字符串(文章内容)作为输入,返回一个区块对象的数组。咱们来一起看看它的主要流程:

  1. 预处理字符串:pre_render_search()

    在正式解析之前,pre_render_search() 会对输入字符串进行一些预处理,主要是清理 HTML 注释,防止误判。例如,它会删除嵌套的注释。

    private function pre_render_search( $content ) {
       // Remove nested HTML comments.
       $content = preg_replace( '/<!--(?!-->).*?<!--/s', '<!--', $content );
    
       return $content;
    }

    这段代码使用正则表达式 '/<!--(?!-->).*?<!--/s' 匹配嵌套的 HTML 注释,并将其替换为 <!--(?!-->) 是一个负向零宽断言,确保匹配的不是结束标签 -->。 这样做的目的是避免解析器误以为嵌套注释是新的区块开始。

  2. 核心解析循环:parse()

    parse() 方法是整个解析过程的核心。它使用正则表达式匹配区块的开始和结束注释,并递归地解析嵌套区块。

    public function parse( $content ) {
       $this->content = $this->pre_render_search( $content );
       $this->blocks  = $this->parse_inner( $this->content );
       return $this->blocks;
    }

    这里 parse_inner() 才是真正干活的函数。

  3. 深入 parse_inner():递归解析的精髓

    parse_inner() 方法负责在给定的字符串中查找区块,并递归地解析嵌套的区块。

    private function parse_inner( $content, $is_root_block = true ) {
       $blocks = array();
       $offset = 0;
    
       while ( preg_match( '/<!-- wp:([a-z0-9-]+(?:[/][a-z0-9-]+)?)({"[^"]*?"(?:,[^"]*?")*})? -->/s', $content, $matches, PREG_OFFSET_CAPTURE, $offset ) ) {
           $block_name_match = $matches[1];
           $block_name       = $block_name_match[0];
           $block_start      = $matches[0][1];
           $block_attributes = isset( $matches[2] ) ? json_decode( $matches[2][0], true ) : array();
    
           //... (省略部分代码) ...
    
           $end_tag_regex = '/<!-- /wp:' . preg_quote( $block_name, '/' ) . ' -->/s';
           if ( preg_match( $end_tag_regex, $content, $end_matches, PREG_OFFSET_CAPTURE, $block_start + strlen( $matches[0][0] ) ) ) {
               $block_end = $end_matches[0][1];
               $inner_content = substr( $content, $block_start + strlen( $matches[0][0] ), $block_end - ( $block_start + strlen( $matches[0][0] ) ) );
               $inner_blocks = $this->parse_inner( $inner_content, false );
    
               $block = array(
                   'blockName'    => $block_name,
                   'attrs'        => $block_attributes,
                   'innerHTML'    => $inner_content,
                   'innerBlocks'  => $inner_blocks,
                   'innerHTML'    => substr( $content, $block_start + strlen( $matches[0][0] ), $block_end - ( $block_start + strlen( $matches[0][0] ) ) ),
                   'innerContent' => array_merge( array( $matches[0][0] ), wp_list_pluck( $inner_blocks, 'innerHTML' ), array( $end_matches[0][0] ) ),
                   'start'        => $block_start,
                   'end'          => $block_end + strlen( $end_matches[0][0] ), // Corrected end position
               );
    
               $blocks[] = $block;
               $offset = $block_end + strlen( $end_matches[0][0] ); // Move offset to the end of the block
           } else {
               //... (处理没有结束标签的情况) ...
           }
       }
    
       return $blocks;
    }

    这段代码做了这些事情:

    • 正则表达式匹配: 使用正则表达式 /<!-- wp:([a-z0-9-]+(?:[/][a-z0-9-]+)?)({"[^"]*?"(?:,[^"]*?")*})? -->/s 匹配区块的开始标签。这个正则比较复杂,咱们来分解一下:
      • <!-- wp:: 匹配区块开始注释的前缀。
      • ([a-z0-9-]+(?:[/][a-z0-9-]+)?): 匹配区块的名称,允许包含字母、数字、短横线和斜杠(用于命名空间)。
      • ({"[^"]*?"(?:,[^"]*?")*})?: 可选的匹配区块的属性,属性必须是有效的JSON。[^"]*? 匹配非引号字符,(?:,[^"]*?")* 允许有多个属性。
      • -->: 匹配区块开始注释的后缀。
    • 提取信息: 从匹配结果中提取区块名称和属性。
    • 查找结束标签: 使用正则表达式 /<!-- /wp:' . preg_quote( $block_name, '/' ) . ' -->/s 查找与当前区块名称匹配的结束标签。 preg_quote() 函数用于转义区块名称中的特殊字符,防止正则表达式出错。
    • 递归解析嵌套区块: 如果找到结束标签,则提取区块的内部内容,并递归调用 parse_inner() 方法解析内部的嵌套区块。
    • 构建区块对象: 将提取的信息和解析结果组装成一个包含以下属性的数组:
      • blockName: 区块名称。
      • attrs: 区块属性(JSON 解码后的数组)。
      • innerHTML: 区块的内部 HTML 内容。
      • innerBlocks: 嵌套的区块对象数组。
      • innerContent: 一个数组,包含开始标签,中间的区块内容,和结束标签,为了方便区块的重组。
      • start: 区块开始的位置。
      • end: 区块结束的位置。
    • 处理错误: 如果没有找到结束标签,则认为该区块是不完整的,并记录错误。
  4. 返回结果:区块对象数组

    parse() 方法最终返回一个区块对象的数组,每个对象都包含了区块的名称、属性、内部 HTML 内容和嵌套的区块。

三、代码示例:模拟 WP_Block_Parser 的解析过程

为了更好地理解 WP_Block_Parser 的工作原理,咱们来写一个简单的 PHP 函数,模拟它的解析过程:

<?php

function parse_blocks( $content ) {
    $blocks = [];
    $offset = 0;

    while ( preg_match( '/<!-- wp:([a-z0-9-]+(?:[/][a-z0-9-]+)?)(s+{"[^"]*?"(?:,[^"]*?")*})?s+-->/s', $content, $matches, PREG_OFFSET_CAPTURE, $offset ) ) {
        $block_name_match = $matches[1];
        $block_name       = $block_name_match[0];
        $block_start      = $matches[0][1];
        $block_attributes = isset( $matches[2] ) ? json_decode( trim($matches[2][0]), true ) : [];

        $end_tag_regex = '/<!-- /wp:' . preg_quote( $block_name, '/' ) . ' -->/s';
        if ( preg_match( $end_tag_regex, $content, $end_matches, PREG_OFFSET_CAPTURE, $block_start + strlen( $matches[0][0] ) ) ) {
            $block_end = $end_matches[0][1];
            $inner_content = substr( $content, $block_start + strlen( $matches[0][0] ), $block_end - ( $block_start + strlen( $matches[0][0] ) ) );

            $block = [
                'blockName'    => $block_name,
                'attrs'        => $block_attributes,
                'innerHTML'    => $inner_content,
                'start'        => $block_start,
                'end'          => $block_end + strlen( $end_matches[0][0] ),
            ];

            $blocks[] = $block;
            $offset = $block_end + strlen( $end_matches[0][0] );
        } else {
            // 找不到结束标签,处理错误
            echo "Error: No closing tag found for block {$block_name}n";
            $offset = $block_start + strlen( $matches[0][0] ); // 继续搜索下一个区块
        }
    }

    return $blocks;
}

// 测试用例
$content = '
<!-- wp:paragraph -->
<p>Hello, world!</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":123,"sizeSlug":"large"} -->
<figure class="wp-block-image size-large"><img src="image.jpg" alt="" class="wp-image-123"/></figure>
<!-- /wp:image -->

<!-- wp:missing-end-tag -->
<p>This block is missing its closing tag.</p>
';

$blocks = parse_blocks( $content );

// 打印解析结果
print_r( $blocks );

?>

这个示例代码虽然简化了很多细节,但基本实现了 WP_Block_Parser 的核心功能。它可以解析简单的区块字符串,提取区块名称、属性和内部 HTML 内容,并返回一个区块对象的数组。

四、WP_Block_Parser 的应用场景

WP_Block_Parser 在 WordPress 中扮演着重要的角色,它的应用场景非常广泛:

  • 文章内容解析: 这是最基本的功能,用于将文章内容中的区块字符串解析为结构化对象,方便 WordPress 进行处理和展示。
  • 区块编辑器: 区块编辑器使用 WP_Block_Parser 将用户在编辑器中创建的区块转换为 HTML 字符串,并存储到数据库中。
  • 主题开发: 主题开发者可以使用 WP_Block_Parser 解析文章内容,并根据区块的类型和属性自定义区块的展示方式。
  • 插件开发: 插件开发者可以使用 WP_Block_Parser 解析文章内容,并根据区块的类型和属性执行自定义的操作,例如,统计文章中某个特定类型区块的数量。

五、总结:WP_Block_Parser 的价值

WP_Block_Parser 是 WordPress 区块系统的核心组件之一。它负责将文章内容中的区块字符串解析为结构化对象,为 WordPress 后续的处理和展示提供了便利。理解 WP_Block_Parser 的工作原理,可以帮助我们更好地理解 WordPress 区块系统的运作机制,并为我们进行主题和插件开发提供指导。

特性 描述
功能 将包含区块标记的字符串解析为结构化的区块对象数组。
核心方法 parse()parse_inner(),后者负责递归解析嵌套区块。
输入 包含区块标记的字符串(通常是文章内容)。
输出 一个区块对象的数组,每个对象包含区块名称、属性、内部 HTML 内容、嵌套区块等信息。
应用场景 文章内容解析、区块编辑器、主题开发、插件开发等。
正则表达式 使用正则表达式匹配区块的开始和结束标签,并提取区块名称和属性。
递归 使用递归的方式解析嵌套的区块。
错误处理 能够检测并处理不完整的区块(例如,缺少结束标签)。

好了,今天的讲座就到这里。希望大家通过今天的学习,对 WP_Block_Parser 有了更深入的了解。下次有机会,咱们再聊聊 WordPress 的其他有趣的技术细节。感谢大家的收看!

发表回复

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