在PHP中如何截断带HTML标签的富文本字符串

自由de单车 2022-05-14 14:36:04 阅读数:210

phphtml标签文本截断

前言

在开发中,截断字符串是一个常见的操作。在PHP中,截断字符串十分方便,使用mb_substr函数就可以。
但这只是针对普通的字符串而言,如果要截断的是一个带HTML标签的富文本字符串,就不能简单的使用这个函数了。
大部分HTML标签都是成对出现的,我们不能在一对标签的中间进行截断,也不能把标签本身截断,否则就会出问题。

代码

为了解决这个问题,我使用了DOMDocument这个类(需要安装libxml扩展)来实现HTML字符串的截断操作,代码如下:

<?php
class HtmlText
{

private static function iterateDOMNodes(DOMNode $domNode, callable $callable)
{

foreach ($domNode->childNodes as $node) {

$callable($node);
if($node->hasChildNodes()) {

static::iterateDOMNodes($node, $callable);
}
}
}
/** * 截断带HTML标签的字符串 * * @param string $string 带HTML标签的字符串 * @param int $limit 要截取的字数 * @param array $option 选项 * - ellipsis:省略符号,默认值... * - strip_attr:是否去除标签的attribute属性,默认值false * * @return string 截断后的字符串 * * @throws DOMException */
public static function truncate(string $string, int $limit, array $option = []): string
{

$default = [
'ellipsis' => '...',
'strip_attr' => false,
];
$option = $option + $default;
$oriDoc = new DOMDocument();
// 将字符串的编码转换成HTML-ENTITIES,防止中文乱码
$convertedString = mb_convert_encoding($string, 'HTML-ENTITIES', 'UTF-8');
@$oriDoc->loadHTML($convertedString, LIBXML_HTML_NODEFDTD);
$newDoc = new DOMDocument();
$newDocXPath = new DOMXPath($newDoc);
static::iterateDOMNodes($oriDoc, function($oriNode) use ($newDoc, $newDocXPath, $option, &$limit) {

if ($limit <= 0) {

return;
}
switch ($oriNode->nodeType) {

case XML_TEXT_NODE:
$oriNodeVal = $oriNode->nodeValue;
if (preg_match('/^[\s\xa0]+$/u', $oriNodeVal)) {

return;
}
$oriNodeVal = str_replace(["\r\n", "\n"], '', $oriNodeVal);
// 前后的空白字符不算入截取字数
if (preg_match('/^([\s\xa0]*)(\S.*\S)([\s\xa0]*)$/u', $oriNodeVal, $matched)) {

$preBlank = $matched[1];
$strNeedCut = $matched[2];
$sufBlank = $matched[3];
} else {

$preBlank = '';
$strNeedCut = $oriNodeVal;
$sufBlank = '';
}
$strLength = mb_strlen($strNeedCut);
if ($strLength >= $limit) {

$strNeedCut = mb_substr($strNeedCut, 0, $limit);
$strNeedCut = "{
$strNeedCut}{
$option['ellipsis']}";
$sufBlank = '';
}
$limit -= $strLength;
$tmp = "{
$preBlank}{
$strNeedCut}{
$sufBlank}";
$newNode = $newDoc->createTextNode($tmp);
break;
case XML_ELEMENT_NODE:
$newNode = $newDoc->createElement($oriNode->nodeName);
if (!$option['strip_attr'] && $oriNode->hasAttributes()) {

foreach ($oriNode->attributes as $attr) {

$newAttr = new DOMAttr($attr->nodeName, $attr->nodeValue);
$newNode->setAttributeNode($newAttr);
}
}
break;
default:
return;
}
if (!$newDoc->hasChildNodes()) {

$newDoc->appendChild($newNode);
} else {

$oriParentNodePath = pathinfo($oriNode->getNodePath(), PATHINFO_DIRNAME);
$parentNode = $newDocXPath->query($oriParentNodePath)->item(0);
$parentNode->appendChild($newNode);
}
});
$newString = html_entity_decode($newDoc->saveHTML());
// 去除自动添加的标签
$checkTags = ['html', 'body', 'head', 'p'];
foreach ($checkTags as $tag) {

if (stripos($string, "<$tag>") === false
&& stripos($newString, "<$tag>") !== false) {

$newString = str_replace(["<$tag>", "</$tag>"], '', $newString);
}
}
return $newString;
}
}

用例测试

用例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>

额…这个用例有问题,中间的<p>标签不见了,并自动在两边添加了一对p标签,不过问题不是很大…

总结

经过测试,大部分用例的测试结果还是正常的,已经能满足我的使用,有一些小问题后续有空再修复。

另外,开源框架cakephp也提供了一个截断HTML富文本的功能类,Text类的truncate方法,详情见github

版权声明:本文为[自由de单车]所创,转载请带上原文链接,感谢。 https://blog.csdn.net/ljfrocky/article/details/124700185