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 处理器中。所以请关注它,并关注本博客的报道。
- WordPress 6.2 中引入 HTML API (Make Core Blog)
- class WP_HTML_Tag_Processor {}
- 进展报告:HTML API (Make Core Blog)
- WP_HTML_Tag_Processor 概述问题 #44410 (GitHub 跟踪问题)