起因
在折腾 icarus 移植主题的时候,想到给文章摘要增加一个“保留摘要样式”的选项(Typecho 默认的摘要摘取策略是:去除所有的 HTML 标签,只保留文本内容)。原以为在 strip_tags
函数调用前做一个判断就可以了,但这样做以后,发现在摘要的末尾莫名地出现了诸如 </br>
</img>
等奇怪的标记。为此稍微折腾了一番,发现了 Typecho_Common::fixHtml
函数的一个处理自闭合标签的 Bug。手动 Patch 了一下,本打算来一个 Pull Request,最后发现开发版已经修复了这个 Bug 了(但正式版 17.10.30 尚未更新,因此此 Bug 截至目前(2019/2/6)仍是尚未修复的状态,在开发主题、插件时需要注意一下)。这里整理了一下当时的一些笔记。
溯源
相关代码(一)
Widget_Abstract_Contents::excerpt()
/**
* 输出文章摘要
*
* @access public
* @param integer $length 摘要截取长度
* @param string $trim 摘要后缀
*/
public function excerpt($length = 100, $trim = '...')
{
echo Typecho_Common::subStr(
strip_tags($this->excerpt), 0, $length, $trim
); // 去除摘要中的 HTML 标签,然后按照 $length 取得指定长度的摘要
}
Widget_Abstract_Contents::___excerpt()
/**
* 获取文章内容摘要
*
* @access protected
* @return string
*/
protected function ___excerpt()
{
// 针对加密文章的特殊流程
if ($this->hidden) {
return $this->text;
// 加密文章的 text 会被修改为密码输入框 直接返回 text 即可
// 参考 Widget_Abstract_Contents::filter()
}
// 取得文章正文
// 支持插件在摘要生成前将处理 正文 -> HTML 的流程
$content = $this->pluginHandle(__CLASS__)
->trigger($plugged)
->excerpt($this->text, $this);
if (!$plugged) {
// 正文处理的默认实现
$content = $this->isMarkdown
? $this->markdown($content)
: $this->autoP($content);
}
// 根据摘要标记进行截断以生成摘要
$contents = explode('<!--more-->', $content);
// 相当于 $excerpt = $contents[0];
list($excerpt) = $contents;
// 支持插件在摘要生成后再进行修改
// 调用 fixHtml 补齐因 explode 截断而丢失的结束标签(如 </p>)
return Typecho_Common::fixHtml(
$this->pluginHandle(__CLASS__)
->excerptEx($excerpt, $this)
);
}
推测
先来排除法。
- 可以推测取得文章正文的代码是正常工作的(否则正文也会出现异常的
</br>
等标签)。 - 另外从功能上暂时排除掉会让内容减少而不是增多的
strip_tags
以及Typecho_Common::subStr
函数。
因此头号嫌疑是剩下来的一个 Typecho_Common::fixHtml
函数。下面进行一个简单的测试:
<?php
$html = <<<HTML
<p>段落<br>第二行<br><img src="http://github.com/"></p>
HTML;
echo Typecho_Common::fixHtml($html);
得到的结果是:
<p>段落<br>第二行<br><img src="http://github.com/"></p></img></br></br>
可以发现的确是 Typecho_Common::fixHtml
函数给摘要追加了 </br>
等错误的结束标签。
相关代码(二)
/**
* 自闭合html修复函数
* 使用方法:
* <code>
* $input = '这是一段被截断的html文本<a href="#"';
* echo Typecho_Common::fixHtml($input);
* //output: 这是一段被截断的html文本
* </code>
*
* @access public
* @param string $string 需要修复处理的字符串
* @return string
*/
public static function fixHtml($string)
{
// Step 1: 去除不完整的起始标签(见“使用方法”)
$startPos = strrpos($string, "<");
if (false == $startPos) {
return $string;
}
$trimString = substr($string, $startPos);
if (false === strpos($trimString, ">")) {
$string = substr($string, 0, $startPos);
}
// Step 2: 关闭非自闭合标签
// 起始标签列表 (<tagName>)
preg_match_all("/<([_0-9a-zA-Z-\:]+)\s*([^>]*)>/is", $string, $startTags);
// 结束标签列表 (</tagName>)
preg_match_all("/<\/([_0-9a-zA-Z-\:]+)>/is", $string, $closeTags);
if (!empty($startTags[1]) && is_array($startTags[1])) {
// 反转起始标签列表顺序(原起始与结束标签列表顺序相反)
krsort($startTags[1]);
$closeTagsIsArray = is_array($closeTags[1]);
foreach ($startTags[1] as $key => $tag) {
$attrLength = strlen($startTags[2][$key]);
if ($attrLength > 0 && "/" == trim($startTags[2][$key][$attrLength - 1])) {
// 若起始标签为自闭合标签则跳过(形如<br />)
continue;
}
// 查找起始标签是否有对应的结束标签
if (!empty($closeTags[1]) && $closeTagsIsArray) {
if (false !== ($index = array_search($tag, $closeTags[1]))) {
unset($closeTags[1][$index]);
continue;
}
}
// 没有对应的结束标签则进行补充
$string .= "</{$tag}>";
}
}
return preg_replace("/\<br\s*\/\>\s*\<\/p\>/is", '</p>', $string);
}
修复
fixHtml
函数的原理是补回缺失的结束标签,但在第44行判断自闭合标签时忽略了 HTML 中自闭合标签不必要带有一个正斜杠,比如换行标签: <br>
<br/>
<br />
三种写法都是正确的,因此单凭 />
进行自闭合标签的判断是有缺陷的。
查询文档发现,HTML 标记中,自闭合标签(称作 空元素)是有限个的,它们分别是 <area>
, <base>
, <br>
, <col>
, <embed>
, <hr>
, <img>
, <input>
, <link>
, <meta>
, <param>
, <source>
, <track>
, <wbr>
因此,要让 fixHtml 正常工作,有以下两个思路:
增加针对自闭合标签的白名单
这个是目前 Typecho 开发版使用的方案。参考 var/Typecho/Common.php - commit c056f6c - typecho/typecho - GitHub
Diff:
$attrLength = strlen($startTags[2][$key]);
if ($attrLength > 0 && "/" == trim($startTags[2][$key][$attrLength - 1])) {
continue;
}
+ // 白名单
+ if (preg_match("/^(area|base|br|col|embed|hr|img|input|keygen".
+ "|link|meta|param|source|track|wbr)$/i", $tag)) {
+ continue;
+ }
if (!empty($closeTags[1]) && $closeTagsIsArray) {
if (false !== ($index = array_search($tag, $closeTags[1]))) {
unset($closeTags[1][$index]);
continue;
}
}
改变 fixHtml 函数的输入
既然 fixHtml
函数不支持 <br>
这种形式的自闭合标签,但支持 <br />
这种形式的自闭合标签,可以想到的一个思路是在调用 fixHtml
函数前对输入进行处理,将 <br>
等替换为 <br />
这种形式。可以利用 Typecho 提供的 excerptEx 插件接口实现。
function voidElementsPatch($excerpt, $widget)
{
return preg_replace(
'/\<(area|base|br|col|embed|hr|img|input|keygen'.
'|link|meta|param|source|track|wbr)([^\>]*?)\s*\/?>/i',
'<\1\2 />',
$excerpt);
}
Typecho_Plugin::factory('Widget_Abstract_Contents')
->excerptEx = 'voidElementsPatch';
参考资料
- Void Elements - Syntax - HTML 5.2 W3C Recommendation
- var/Typecho/Common.php - commit c056f6c - typecho/typecho - GitHub
- Issue #636 - typecho/typecho - GitHub
- 1.1新版添加摘要符号后会出现好多换行符 - Typecho Forum
本文采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。
本文作者:KeNorizon
本文链接:https://kenorizon.cn/code/typecho-fix-html-func-bug.html