社区新闻

HTML API:处理标签,而非痛苦

查看官方原文 ↗ 发布于

如果你经常使用正则表达式解析 HTML,那么你在 WordPress 技术栈方面一定拥有相当令人印象深刻的技能。

它们确实有效。大多数时候。但代码中的模式看起来非常奇怪,就像在古墓墙上发现的凿刻文字。

而当它们失效时?事情会迅速变得严重,到处是破坏和漏洞。

6.2 新增,持续发展

WordPress 6.2 新增了 HTML API 的第一阶段——HTML 标签处理器。仅此处理器本身就比正则表达式更好。它方便、可靠、快速,并且易于阅读。

看看下面的两个示例——首先是正则表达式,然后是 HTML 标签处理器。这些恰好是促使贡献者最初构建处理器的用例。你自己可能也会想到很多使用处理器的方法。

示例 1:如何使用 loading 属性实现图片懒加载

首先:使用正则表达式的方式

当你得到想要的结果时,很容易忘记痛苦。但希望这次回顾困难的方式能说服你,尽可能避免痛苦会更好,尤其是如果你能得到更好的结果。请注意,正则表达式方式需要两次迭代和一些清理:

迭代 1:一个懒加载图片模式

让我们构建一个执行三件事的模式:

  • 匹配图片标签
  • 捕获其属性
  • 将 loading 属性附加到代码片段的末尾
function render_block( $html ) {
    return preg_replace(
        '~<img(.*)>~',
        '<img$1 loading="lazy">',
        $html
    );
}

现在,如果你的标记中只有一个图片标签,这可以正常工作。

但是,如果超过一个,模式将开始一次性消耗所有匹配的字符(在正则表达式中,技术术语是“贪婪”,但这并非价值判断,而是一种观察)。

所以这行代码会将 loading 属性添加到文档中的最后一个标签。

The <img> tag is a <code>void element.

迭代 2:一点隧道视野

你可以通过将 . 改为 .*? 来解决眼前的问题,这有点像通过使用 ~<img(.*?)>~ 跳过所有不是 > 的内容。

function render_block( $html ) {
    return preg_replace(
        '~<img(.*?)>~',
        '<img$1 loading="lazy">',
        $html
    );
}

但是等等。> 可以出现在 HTML 属性内部,像这样:

<img title="bears > tigers">

并给你一些严重损坏的标记。

<img title="bears  loading="lazy"> tigers">

现在,模式识别可能是 HTML 标签一部分的属性:如果子匹配组看到一个属性名、一个 = 和一个带引号的值在一起,那就是标签的一部分,模式会跳过它。

但它没有。

function render_block( $html ) {
    return preg_replace(
        '~<img(.*?)([a-z-]+="[^"]*"s*)*>~,
        '<img$1$2 loading="lazy">',
        $html
    );
}

现在事情变得复杂了,而且模式甚至没有捕获使用单引号、没有引号或者没有 = 号的属性。

这已经有很多情况需要检查了。你可能会漏掉一些。

<img((?:s+[a-z-]+(?:=(?:w+|(['"]).*$2))?)*)>

真是一团糟。看起来令人印象深刻。

经过所有这些迭代,谁能看出你最初想做什么?你处理了所有的边缘情况。

但你的代码处理了吗?

你已经经历了三次。迭代 #3 并不比你开始时好多少。如果已经有其他东西设置了 loading 属性,浏览器(或此时解析标记的任何东西)将忽略你真正想要使用的那个,因为它重复了。

一些常见的属性命名方案会破坏这一点。有时 DOMDocument 甚至无法正常工作。

<textarea>

There is no <img> inside a textarea because everything inside is plaintext.

</textarea>

<script>

console.log( "This also contains no <img> because it's inside a script." );

</script>
This contains an image but because it already has an earlier definition of the loading attribute, any duplicates appended to the end will be ignored by the browser.

<img loading=eager src="machine-rusty.jpg">

然后还有这个:假设正则表达式模式中的这一个点设法理解了整个 HTML 语法及其所有语义处理规则。

还有更多地方需要如此——多得多。在你的代码长度中,有成千上万处——以许多不同的方式完成这项工作。

你真的想监控大约 1575 个不同的实例,寻找然后必须修补和跟踪的错误吗?(编者注:你的数字可能不同。每次都不同。)

我想不是。

用 HTML API 表达意图

如果你准备好尝试正则表达式的替代方案,你需要提升你的思维方式。正则表达式让你思考字符、空格和引号——非常基础的字符串级别的东西。

HTML 标签处理器将你的视角提升并拉出——到一个更全局的方法,处理标签和属性名以及属性值。

看看:

迭代 1:向图片添加 lazy 属性

function render_block( $html ) {

    $processor = new WP_HTML_Tag_Processor( $html );

    if ( $processor->next_tag( 'img' ) ) {

        $processor->set_attribute( 'loading', 'lazy' );

    }

    return $processor->get_updated_html();

}

这是第一次迭代,也是最后一次——你一次性完成了工作。它快速、健壮且安全。

欢迎使用标签处理器

它是新 HTML API 中的第一个接口。它有一个工作:查找然后读取或更改特定 HTML 标签的属性。

以下是使用方法。

  • 创建标签处理器的新实例,并添加要处理的输入 HTML。
  • 调用 next_tag() 来查找并匹配你想要的下一标签。
  • 调用 get_attribute()set_attribute() 来读取和修改匹配标签上的属性。

完成后,调用 get_updated_html() 来返回(或执行其他需要的操作)转换后的输出。

就这样!

示例 2:如何向具有特定区块属性的区块添加类

首先,再看一下困难的方式——扩展正则表达式。以防你正想这么做。

迭代 1:在图片标签末尾添加类

preg_replace(
    '~<img(.*?)class="([^"]*)"([^>]*)>~',
    '<img$1 class="$2 wp-full-width"$3>',
    $html
);

这第一次迭代还算清晰。它试图将 wp-full-width 添加到图片标签的 `class` 属性末尾。

在这个例子中,先别管单引号和双引号以及重复属性的问题。就当这些都没问题。

如果其他一些属性的名称部分匹配你想要处理的属性——但不完全匹配!——事情已经开始出错了。

<img src="machine-rusty.jpg" data-image-class="black-and-white">

上面的代码变成了下面的代码。

正则表达式不仅没有添加类。它还搞乱了你自定义数据属性中需要的应用程序逻辑。

<img src="machine-rusty.jpg" data-image-class="black-and-white wp-full-width">

修复通常有两个步骤:

  • 要求在 class 前有一个空格,使用 ~<img(.*?) class="([^"]*)"([^>]*)>~
  • 盯着它看大约十秒钟后,意识到许多属性前面都有某种空白。还有换行符。第二步中有一个:~<img(.*?)([ tfrn]+)class="([^"]*)"([^>]*)>~

情况好一些了。但整个修复依赖于一个现有的 class 属性。没有类?没有修复。

迭代 2:外部化逻辑

因为正则表达式带来的问题和解决的问题一样多,许多开发人员会提取匹配项,在表达式外部运行逻辑,然后将代码重新拼接起来。

听起来有趣吗?

if ( 1 !== preg_match(
    // 获取 IMG 标签内的属性
    '~<img([^>]*)>~i',
    $html,
    $img_match
) ) {
    return $html;
}

if ( 1 !== preg_match(
    // 查找现有的 class 属性。
    '~[ \t\f\r\n]+class=(['"]).*?1~i',
    $img_match[1],
    $class_match,
    PREG_OFFSET_CAPTURE
) ) {
    return preg_replace( '~<img([^>]*)>~i', '<img class="wp-full-width" $1>', $html );
}

$class  = $class_match[2][0];
$offset = $class_match[2][1];

$before = substr( $html, 0, $offset );
$after  = substr( $html, $offset + strlen( $class ) );

return $before . $class . " wp-full-width" . $after;

你只是想添加一个类名!

但不行:首先你必须处理 HTML 语法。而且代码仍然会在太多常见的 HTML 输入中出错。

任何事情都应该这么难吗?

这里有个想法:如果你使用单词 `class` 来添加一个类呢?(这就是 HTML 用来表示类的方式。想想看。😜)

那么,标签处理器会做什么?

function add_additional_class( $html, $block ) {
    if ( ! isset( $block->attrs['additional_class'] ) ) {
        return $html;
    }

    $processor = new WP_HTML_Tag_Processor( $html );
    if ( $processor->next_tag( 'img' ) ) {
        $processor->add_class( $block->attrs['additional_class'] );
    }

    return $processor->get_updated_html();
}

add_filter( 'render_block', 'add_additional_class', 10, 2 );

嗯,看看这个。

用于 CSS 类的标签处理器函数

标签处理器为你提供了两个用于 CSS 类的辅助函数:<a href="https://developer.wordpress.org/reference/classes/wp_html_tag_processor/add_class/">add_class()</a><a href="https://developer.wordpress.org/reference/classes/wp_html_tag_processor/remove_class/">remove_class()</a>

你大概能猜到它们的作用。你可以将需要的类添加到需要它的标签上——即使该标签已经有一个 class 属性。(如果没有,处理器会添加一个。)

如果 class 属性已经有一个与你尝试添加的名称相同的类?标签处理器将保持标记不变。

多么令人耳目一新!

HTML API 生成的标记能满足你的需求。无需围绕语法、大小写敏感或命名约定等进行大量繁琐操作。也无需无尽的错误跟踪。

当你用 HTML API 替换正则表达式时,你的代码将与其意图一样长久。

插件中的 HTML 错误可以在 HTML 中修复,而无需任何人接触插件逻辑。(编者注:为什么这很神奇?这应该是正常的。)

任何使用该 API 的代码默认都会专注于该代码的意图,而不是其方法的细枝末节。

更多学习资源

现在或将来要处理 HTML?请阅读标签处理器的文档。这是你可以用该 API 做的第一件事,但还有更多内容即将推出。

随着未来版本对 API 的补充,其新功能将出现在恰如其名的 HTML 处理器中。所以请关注它,并关注本博客的报道。