在HTML字符串中自动换行/截断文本

3

我想做的是:我有一个包含HTML标签的字符串,我想使用wordwrap函数对其进行裁剪,但要排除HTML标签。

我卡住了:

public function textWrap($string, $width)
{
    $dom = new DOMDocument();
    $dom->loadHTML($string);
    foreach ($dom->getElementsByTagName('*') as $elem)
    {
        foreach ($elem->childNodes as $node)
        {
            if ($node->nodeType === XML_TEXT_NODE)
            {
                $text = trim($node->nodeValue);
                $length = mb_strlen($text);
                $width -= $length;
                if($width <= 0)
                { 
                    // Here, I would like to delete all next nodes
                    // and cut the current nodeValue and finally return the string 
                }
            }
        }
    }
}

我不确定我现在做的是否正确。希望清楚易懂...

编辑:

这里有一个例子。我有这个文本。

    <p>
        <span class="Underline"><span class="Bold">Test to be cut</span></span>
   </p><p>Some text</p>

假设我想在第6个字符处截断它,我希望返回这个结果:

<p>
    <span class="Underline"><span class="Bold">Test to</span></span>
</p>

1
你现在做得不对。一个好的开始是学习nodeValue代表什么。此外,XML有子元素,我认为你实际上想要在某个位置删除所有子元素。因此,请尝试开发一下HTML中的自动换行功能。另外,你可能需要添加一些简单的输入HTML示例,以便更清楚地了解你遇到的问题。 - hakre
我已经在一个相关的答案中添加了一些代码,它执行类似的操作(基于文本表示形式的字符串来操作DOM)(问题:忽略preg_replace中的HTML标签)。希望这对你有所帮助,但可能对初学者来说有点困难,不过我认为它仍然可以提供一些提示。 - hakre
首先,我想说你不知道如何找到DOM分割的位置。在你的例子中,HTMLHEADBODY标签是否像HTML 2一样自动生成?只是为了更好地理解你的示例代码而问一下。 - hakre
没有HTMLHEAD或者BODY标签,只有spanapulolliimg可用。我只是使用这些标签格式化了一些文本。由于我的代码限制了文本的最大长度,所以我认为需要分割DOM。事实上,我对DOM库还很陌生,不知道如何正确地切割DOM。我想要包装文本,但同时也希望在最后一个元素中折叠文本之后将所有其他子元素删除。 - Peekyou
其中一部分是拆分右侧的 DOMText 节点,然后删除所有后续节点及其子节点。在此过程中,xpath axis 可能会有所帮助(使用 following-sibling 和/或 following)。 - hakre
显示剩余6条评论
2个回答

3
正如我在评论中所写的那样,您首先需要找到文本偏移量来进行截取。
首先,我设置了一个包含HTML片段的DOMDocument,然后选择代表它的body元素。
$htmlFragment = <<<HTML
<p>
        <span class="Underline"><span class="Bold">Test to be cut</span></span>
   </p><p>Some text </p>
HTML;

$dom = new DOMDocument();
$dom->loadHTML($htmlFragment);
$parent = $dom->getElementsByTagName('body')->item(0);
if (!$parent)
{
    throw new Exception('Parent element not found.');
}

然后我使用我的TextRange类找到需要剪切的位置,使用TextRange实际进行剪切并定位应成为片段最后一个节点的DOMNode:

$range = new TextRange($parent);

// find position where to cut the HTML textual represenation
// by looking for a word or the at least matching whitespace
// with a regular expression. 
$width = 17;
$pattern = sprintf('~^.{0,%d}(?<=\S)(?=\s)|^.{0,%1$d}(?=\s)~su', $width);
$r = preg_match($pattern, $range, $matches);
if (FALSE === $r)
{
    throw new Exception('Wordcut regex failed.');
}
if (!$r)
{
    throw new Exception(sprintf('Text "%s" is not cut-able (should not happen).', $range));
}

这个正则表达式可以找到在 $range 的文本表示中切割的偏移量。该正则表达式模式 受另一个答案启发,该答案对其进行了更详细的讨论,并稍作修改以适应此答案的需要。
// chop-off the textnodes to make a cut in DOM possible
$range->split($matches[0]);
$nodes = $range->getNodes();
$cutPosition = end($nodes);

由于有可能没有内容可供剪切(例如,body 可能为空),我需要处理这种特殊情况。否则,如注释中所述,所有 后续 节点都需要被删除:

// obtain list of elements to remove with xpath
if (FALSE === $cutPosition)
{
    // if there is no node, delete all parent children
    $cutPosition = $parent;
    $xpath = 'child::node()';
}
else
{
    $xpath = 'following::node()';
}

其余部分很简单:查询xpath,删除节点并输出结果:
// execute xpath
$xp = new DOMXPath($dom);
$remove = $xp->query($xpath, $cutPosition);
if (!$remove)
{
    throw new Exception('XPath query failed to obtain elements to remove');
}

// remove nodes
foreach($remove as $node)
{
    $node->parentNode->removeChild($node);
}

// inner HTML (PHP >= 5.3.6)
foreach($parent->childNodes as $node)
{
    echo $dom->saveHTML($node);
}

完整的代码示例包括 TextRange 类,可以在 viper codepad 上 获得。由于 codepad 存在一个 bug,因此其结果不正确(相关内容请参见 XPath 查询结果顺序)。实际输出如下所示:
<p>
        <span class="Underline"><span class="Bold">Test to</span></span></p>

所以请确保您有当前的libxml版本(通常是这种情况),并且最后输出中使用了PHP函数saveHTML,该参数自PHP 5.3.6起可用。如果您没有那个PHP版本,请使用其他替代方法,如How to get the xml content of a node as a string?或类似的问题。
当您仔细查看我的示例代码时,您可能会注意到截断长度相当大($width = 17;)。这是因为文本前面有许多空格字符。可以通过使正则表达式在其前面放弃任意数量的空格和/或修剪TextRange来调整它。第二个选项需要更多功能,我编写了一些快速的内容,可在创建初始范围后使用:
...
$range = new TextRange($parent);
$trimmer = new TextRangeTrimmer($range);
$trimmer->trim();
...

这将去除您的HTML片段中左侧和右侧不必要的空格。以下是TextRangeTrimmer代码:

class TextRangeTrimmer
{
    /**
     * @var TextRange
     */
    private $range;

    /**
     * @var array
     */
    private $charlist;

    public function __construct(TextRange $range, Array $charlist = NULL)
    {
        $this->range = $range;
        $this->setCharlist($charlist);      
    }
    /**
     * @param array $charlist list of UTF-8 encoded characters
     * @throws InvalidArgumentException
     */
    public function setCharlist(Array $charlist = NULL)
    {
         if (NULL === $charlist)
            $charlist = str_split(" \t\n\r\0\x0B")
        ;

        $list = array();

        foreach($charlist as $char)
        {
            if (!is_string($char))
            {
                throw new InvalidArgumentException('Not an Array of strings.');
            }
            if (strlen($char))
            {
                $list[] = $char; 
            }
        }

        $this->charlist = array_flip($list);
    }
    /**
     * @return array characters
     */
    public function getCharlist()
    {
        return array_keys($this->charlist);
    }
    public function trim()
    {
        if (!$this->charlist) return;
        $this->ltrim();
        $this->rtrim();
    }
    /**
     * number of consecutive charcters of $charlist from $start to $direction
     * 
     * @param array $charlist
     * @param int $start offset
     * @param int $direction 1: forward, -1: backward
     * @throws InvalidArgumentException
     */
    private function lengthOfCharacterSequence(Array $charlist, $start, $direction = 1)
    {
        $start = (int) $start;              
        $direction = max(-1, min(1, $direction));
        if (!$direction) throw new InvalidArgumentException('Direction must be 1 or -1.');

        $count = 0;
        for(;$char = $this->range->getCharacter($start), $char !== ''; $start += $direction, $count++)
            if (!isset($charlist[$char])) break;

        return $count;
    }
    public function ltrim()
    {
        $count = $this->lengthOfCharacterSequence($this->charlist, 0);

        if ($count)
        {
            $remainder = $this->range->split($count);
            foreach($this->range->getNodes() as $textNode)
            {
                $textNode->parentNode->removeChild($textNode);
            }
            $this->range->setNodes($remainder->getNodes());
        }

    }
    public function rtrim()
    {
        $count = $this->lengthOfCharacterSequence($this->charlist, -1, -1);

        if ($count)
        {
            $chop = $this->range->split(-$count);
            foreach($chop->getNodes() as $textNode)
            {
                $textNode->parentNode->removeChild($textNode);
            }
        }
    }
}

希望这有所帮助。

Codepad Viper挂了 :(,TextRange和TextRangeTrimmer类的源代码也在这里:https://gist.github.com/gists/1894360/ - hakre
1
hakre在2012年2月23日发布的链接已经失效。这里是一个似乎有效的新链接:https://gist.github.com/hakre/1894360 - Jon Watson
@JonWatson:没错,就是那个例子。看起来 GitHub 改变了 gist URI,感谢你的努力! - hakre

0

如果你不需要使用DOM解析,只需要截取HTML——看一下这个Gist中的cot_string_truncate函数。它来自Cotonti CMF。

它可以处理纯文本或HTML。你可以设置长度,并选择如何截断文本——通过限制确切的字符数或最近边界的单词。

它适当地对待HTML实体和连续空格字符(在浏览器中查看)——所以你的例子应该可以很好地工作:

$test_str = "<p>
    <span class=\"Underline\"><span class=\"Bold\">Test to be cut</span></span>
</p><p>Some text</p>";

echo cot_string_truncate($test_str, 8);

结果:

<p>
     <span class="Underline"><span class="Bold">Test to</span></span></p>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接