前言
在开发中,截断字符串是一个常见的操作。在PHP中,截断字符串十分方便,使用mb_substr函数就可以。
但这只是针对普通的字符串而言,如果要截断的是一个带HTML标签的富文本字符串,就不能简单的使用这个函数了。
大部分HTML标签都是成对出现的,我们不能在一对标签的中间进行截断,也不能把标签本身截断,否则就会出问题。
代码
<?php
class HtmlText
{
/**
* 截断带HTML标签的字符串
*
* @param string $text 带HTML标签的字符串
* @param int $length 要截取的字数
* @param string $ellipsis 省略符号
*
* @return string 截断后的字符串
*/
public static function truncate(string $text, int $length, string $ellipsis = '...'): string
{
$text = str_replace(["\r\n", "\n"], '', $text);
$openTagsBucket = [];
$result = '';
$loop = true;
$isCut = false;
while ($loop) {
if (preg_match('/^</', $text)) {
// 寻找下一个">"字符的位置
$nextGtCharPos = mb_strpos($text, '>');
if ($nextGtCharPos !== false) {
$splitPos = $nextGtCharPos + 1;
// 将<>之间的内容截取出来,并判断是否是合法的HTML标签
$isTag = false;
$tagStr = mb_substr($text, 0, $splitPos);
if (preg_match('/^<([a-z]+[1-9]*).*>$/i', $tagStr, $matches)) {
array_unshift($openTagsBucket, [
'tag' => $matches[0],
'tag_name' => strtolower($matches[1]),
]);
$isTag = true;
} elseif (preg_match('/^<\/([a-z]+[1-9]*)>$/i', $tagStr, $matches)) {
if (isset($openTagsBucket[0])
&& strcasecmp($openTagsBucket[0]['tag_name'], $matches[1]) == 0) {
array_shift($openTagsBucket);
}
$isTag = true;
}
// 如果是HTML标签,则直接拼接
if ($isTag) {
$result .= $tagStr;
$text = mb_substr($text, $splitPos);
continue;
}
}
}
if ($text === '') {
break;
}
$nextLtCharPos = mb_strpos($text, '<', 1);
if ($nextLtCharPos === false) {
$loop = false;
$normalStr = $text;
} else {
$normalStr = mb_substr($text, 0, $nextLtCharPos);
$text = mb_substr($text, $nextLtCharPos);
}
$normalStrLen = mb_strlen($normalStr);
if ($normalStrLen >= $length) {
$loop = false;
$isCut = true;
$subStr = mb_substr($normalStr, 0, $length);
$result .= "{$subStr}{$ellipsis}";
} else {
$result .= $normalStr;
$length -= $normalStrLen;
}
}
// 字符串被截断才需要补闭合标签
if ($isCut) {
foreach ($openTagsBucket as $item) {
// 忽略自闭合标签
if (in_array($item['tag_name'], ['link', 'meta', 'base', 'img', 'input', 'br', 'hr'])) {
continue;
}
$result .= "</{$item['tag_name']}>";
}
}
return $result;
}
}
用例测试
用例1:
<?php
$text =<<<EOT
<div>
<script src="jquery-2.1.1.min.js"></script>
<p style="color: red;">
<a href="#">床前明月光</a>
</p>
<p>疑似地上霜</p>
<img src="jquery-2.1.1.min.js" alt=""/>
<h2>举头望明月</h2>
</div>
EOT;
// 截取8个字
echo HtmlText::truncate($text, 8);
输出:
<div>
<script src="jquery-2.1.1.min.js"></script>
<p style="color: red;">
<a href="#">床前明月光</a>
</p>
<p>疑似地...</p>
</div>
刚好是8个字,HTML标签也没有被截断,并自动在末尾拼接上了...
省略号,正常。
用例2:
$text = '这是一段不带标签的文本';
echo HtmlText::truncate($text, 8); // 输出:这是一段不带标签...
也可以用于不带HTML标签的文本
用例3:
$text = '这是一段只<p>有一个标签的文本';
echo HtmlText::truncate($text, 8); // 输出:这是一段只<p>有一个...</p>
其它
另外,开源框架cakephp
也提供了一个截断HTML富文本的功能类,Text
类的truncate
方法,详情见github。