剖析 WordPress `WP_Block_Parser` 类的源码:它如何将文章的 HTML 内容解析成区块对象数组。

各位观众老爷,大家好!今天咱们来聊聊 WordPress 里面一个相当重要的家伙,它就是 WP_Block_Parser 类。这家伙的任务可不轻,得把文章那堆乱七八糟的 HTML 内容,像剥洋葱一样一层层扒开,最终变成一个个区块对象,方便 WordPress 后续处理。

准备好了吗?咱们这就开始“解剖”它,看看它到底是怎么工作的。

1. 什么是区块?为什么要解析?

首先,得明确一下,什么是 WordPress 的“区块”。简单来说,区块就是 WordPress 古腾堡编辑器(Gutenberg)的核心。它把文章内容拆分成独立的、可重复使用的单元,比如段落、标题、图片、列表等等。

为什么要解析呢?因为 WordPress 需要理解文章的内容结构,才能进行渲染、保存、搜索等操作。直接操作原始 HTML 字符串效率太低,而且容易出错。把 HTML 转换成结构化的区块对象,就方便多了。

2. WP_Block_Parser 类的概览

WP_Block_Parser 类位于 wp-includes/class-wp-block-parser.php 文件中。它的主要职责是将一段 HTML 字符串解析成一个包含 WP_Block 对象的数组。

简单来说,就像一个工厂,输入是一堆 HTML 原材料,输出是一堆加工好的区块“零件”。

3. 核心方法:parse()

parse() 方法是 WP_Block_Parser 类的核心,也是我们今天要重点分析的对象。它接受一个 HTML 字符串作为输入,返回一个区块对象数组。

它的基本流程是:

  1. 扫描字符串: 找出所有可能的区块起始和结束标记。
  2. 构建树状结构: 根据区块的嵌套关系,构建一个区块树。
  3. 创建区块对象: 将树中的每个节点转换成一个 WP_Block 对象。

4. 深入 parse() 方法

我们来模拟一个简化的 parse() 方法,以便更好地理解其工作原理。请注意,这只是一个简化版本,真实的代码要复杂得多。

<?php

class Simplified_WP_Block_Parser {

    public function parse( $content ) {
        $blocks = [];
        $tokens = $this->tokenize( $content );
        $block_stack = []; // 追踪嵌套区块

        foreach ( $tokens as $token ) {
            if ( $token['type'] === 'block-start' ) {
                // 新区块开始
                $block_name = $token['block_name'];
                $block = [
                    'blockName' => $block_name,
                    'attrs'     => [],
                    'innerBlocks' => [],
                    'innerHTML' => '',
                    'innerContent' => [ '' ],
                ];

                if ( empty( $block_stack ) ) {
                    // 根区块
                    $blocks[] = &$block; // 使用引用,以便后续修改
                    $block_stack[] = &$block;
                } else {
                    // 嵌套区块
                    $parent_block = &$block_stack[count( $block_stack ) - 1];
                    $parent_block['innerBlocks'][] = &$block;
                    $block_stack[] = &$block;
                }

                // 处理属性
                if(isset($token['attrs'])){
                    $block['attrs'] = $token['attrs'];
                }

            } elseif ( $token['type'] === 'block-end' ) {
                // 区块结束
                $closing_block_name = $token['block_name'];

                if ( ! empty( $block_stack ) ) {
                    // 检查栈顶的区块是否是我们要关闭的区块
                    $current_block = &$block_stack[count( $block_stack ) - 1];

                    if ( $current_block['blockName'] === $closing_block_name ) {
                        array_pop( $block_stack ); // 移除栈顶区块
                    } else {
                        // 如果区块不匹配,说明 HTML 结构有问题,可以报错或者忽略
                        // 这里简单地忽略
                    }
                }
            } elseif ($token['type'] === 'text'){
                //文本内容,需要判断当前是否有父级block
                if (!empty($block_stack)) {
                    $current_block = &$block_stack[count( $block_stack ) - 1];
                    $current_block['innerHTML'] .= htmlspecialchars($token['content'], ENT_QUOTES, 'UTF-8');

                    //将文本内容添加到 innerContent 数组中
                    $current_block['innerContent'][0] .= htmlspecialchars($token['content'], ENT_QUOTES, 'UTF-8');
                }

            }
        }

        return $blocks;
    }

    private function tokenize( $content ) {
        $tokens = [];
        $regex = '/(<!--s*wp:([a-z0-9-/]+)s*({.*})?s*-->)|(<!--s*/wp:([a-z0-9-/]+)s*-->)|(.*?(?=<!--wp:|z))/s'; // 匹配区块开始、结束和文本

        preg_match_all( $regex, $content, $matches, PREG_SET_ORDER );

        foreach ( $matches as $match ) {
            if ( isset( $match[2] ) ) {
                // 区块开始
                $block_name = $match[2];
                $attrs_json = isset($match[3]) ? trim($match[3]) : '';
                $attrs = [];
                if($attrs_json){
                  $attrs = json_decode($attrs_json, true);

                  if (json_last_error() !== JSON_ERROR_NONE) {
                      // 处理 JSON 解析错误
                      error_log('JSON 解析错误:' . json_last_error_msg());
                      $attrs = []; // 忽略错误的属性
                  }

                }

                $tokens[] = [
                    'type' => 'block-start',
                    'block_name' => $block_name,
                    'attrs' => $attrs,
                ];
            } elseif ( isset( $match[5] ) ) {
                // 区块结束
                $block_name = $match[5];
                $tokens[] = [
                    'type' => 'block-end',
                    'block_name' => $block_name,
                ];
            } else {
                // 文本内容
                $text = trim($match[6]);
                if($text !== ''){
                    $tokens[] = [
                        'type' => 'text',
                        'content' => $text,
                    ];
                }

            }
        }

        return $tokens;
    }
}

// 示例用法
$content = '<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">Hello, world!</p>
<!-- /wp:paragraph -->

<!-- wp:group -->
<!-- wp:paragraph -->
<p>This is a paragraph inside a group.</p>
<!-- /wp:paragraph -->
<!-- /wp:group -->';

$parser = new Simplified_WP_Block_Parser();
$blocks = $parser->parse( $content );

echo '<pre>';
print_r( $blocks );
echo '</pre>';

?>

简化版的 parse() 方法详解:

  • tokenize() 方法: 这个方法负责将 HTML 字符串分解成一个个的 "token",也就是区块的开始标记、结束标记和文本内容。它使用正则表达式来匹配这些标记。返回一个包含 token 信息的数组。
  • 循环处理 token: parse() 方法遍历 tokenize() 方法返回的 token 数组,根据 token 的类型执行不同的操作。
    • block-start: 遇到区块开始标记,创建一个新的区块数组。如果当前没有父区块($block_stack 为空),则将该区块添加到根区块数组 $blocks 中。否则,将该区块添加到当前父区块的 innerBlocks 数组中。
    • block-end: 遇到区块结束标记,从 $block_stack 中移除对应的区块。如果 $block_stack 为空,说明 HTML 结构有问题,可以忽略或者报错。
    • text: 遇到文本内容,将其添加到当前父区块的 innerHTMLinnerContent 数组中。innerHTML 存储原始 HTML 文本,innerContent 存储处理后的内容。
  • $block_stack: 这个数组用于追踪嵌套区块。每当遇到一个新的区块开始标记,就将该区块添加到 $block_stack 中。当遇到该区块的结束标记时,就从 $block_stack 中移除该区块。

tokenize() 方法中的正则表达式解释:

/(<!--s*wp:([a-z0-9-/]+)s*({.*})?s*-->)|(<!--s*/wp:([a-z0-9-/]+)s*-->)|(.*?(?=<!--wp:|z))/s

这个正则表达式有点复杂,我们来拆解一下:

部分 解释
(<!--s*wp:([a-z0-9-/]+)s*({.*})?s*-->) 匹配区块的开始标记。<!-- wp:block-name {"attribute":"value"} -->,其中 block-name 是区块的名称,{"attribute":"value"} 是区块的属性(可选)。
(<!--s*/wp:([a-z0-9-/]+)s*-->) 匹配区块的结束标记。<!-- /wp:block-name -->,其中 block-name 是区块的名称。
(.*?(?=<!--wp:|z)) 匹配文本内容。.*?(?=<!--wp:|z) 表示匹配任意字符(非贪婪模式),直到遇到 <!--wp: 或者字符串结尾 z
/s s 模式修正符,使 . 可以匹配包括换行符在内的所有字符。

5. WP_Block 对象

WP_Block 对象是 WP_Block_Parser 解析的最终结果。它包含了区块的所有信息,包括:

  • blockName: 区块的名称,比如 core/paragraphcore/image 等。
  • attrs: 区块的属性,以数组形式存储。
  • innerBlocks: 当前区块包含的子区块,也是一个 WP_Block 对象数组。
  • innerHTML: 当前区块的原始 HTML 内容。
  • innerContent: 一个数组,其中包含当前区块的各个组成部分(文本和子区块)。 这个属性在渲染区块时非常有用。它允许遍历区块的内容,并根据需要进行处理。

6. 真实代码中的复杂性

上面我们只是模拟了一个简化的 parse() 方法。真实的代码要复杂得多,因为它需要处理各种特殊情况,比如:

  • 无效的 HTML 结构: 比如区块开始标记和结束标记不匹配,或者区块嵌套错误。
  • 自闭合区块: 比如 <!-- wp:image {"id":123} /-->
  • 动态区块: 动态区块的内容不是静态的 HTML,而是由 PHP 代码动态生成的。
  • HTML 注释: 需要区分真正的区块标记和普通的 HTML 注释。
  • 属性的解析:真实代码对属性的解析要严谨得多,需要处理各种数据类型和格式。
  • 错误处理:真实代码会包含更完善的错误处理机制,以便在解析过程中发现问题并及时报告。

7. 性能考虑

WP_Block_Parser 需要处理大量的 HTML 字符串,因此性能非常重要。WordPress 采取了一些措施来提高解析效率,比如:

  • 使用正则表达式进行快速匹配: 正则表达式引擎经过高度优化,可以快速地在字符串中查找模式。
  • 避免不必要的字符串复制: 字符串复制会消耗大量的内存和 CPU 时间。WordPress 尽量避免不必要的字符串复制操作。
  • 缓存解析结果: 对于静态内容,可以将解析结果缓存起来,避免重复解析。

8. 总结

WP_Block_Parser 类是 WordPress 区块编辑器的重要组成部分。它负责将 HTML 字符串解析成结构化的区块对象,为 WordPress 后续处理内容提供了便利。

虽然它的代码比较复杂,但是通过理解其基本原理和核心方法,我们可以更好地理解 WordPress 区块编辑器的工作方式。

希望今天的“解剖”对你有所帮助! 记住,编程就像剥洋葱,一层一层地剥开,总能找到核心。下次有机会,咱们再聊聊 WordPress 的其他有趣的东西。

发表回复

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