【问题标题】:How to conditionally wrap together elements using the DOM API?如何使用 DOM API 有条件地将元素包装在一起?
【发布时间】:2019-07-18 05:11:38
【问题描述】:

假设我们有这个输入:

<div wrap>1</div>
<div>2</div>
<div wrap>3</div>
<div wrap>4</div>
<div wrap>5</div>

所需的输出应该是:

<div class="wrapper">
  <div wrap>1</div>
</div>
<div>2</div>
<div class="wrapper">
  <div wrap>3</div>
  <div wrap>4</div>
  <div wrap>5</div>
</div>

另外,假设这些元素是 body 元素的直接子元素,并且在它们之前或之后可以有其他不相关的元素或文本节点。

注意连续元素是如何在单个包装器中分组而不是单独包装的。

您将如何处理 body 的 DOMNodeList 并将包装器插入正确的位置?

在关于仅包装 body 元素的直接子元素的对话 (cmets) 之后,

对于这个输入:

<body>
  <div wrap>1
    <div wrap>1.1</div>
  </div>
  <div>2</div>
  <div wrap>3</div>
  <div wrap>4</div>
  <div wrap>5</div>
</body>

所需的输出应该是:

<body>
  <div class="wrapper">
    <div wrap>1
      <div wrap>1.1</div>
      <!–– ignored ––>.
    </div>
  </div>
  <div>2</div>
  <div class="wrapper">
    <div wrap>3</div>
    <div wrap>4</div>
    <div wrap>5</div>
  </div>
</body>

请注意, body 元素的直接后代元素是如何被完全忽略的。

【问题讨论】:

标签: php xml dom xpath


【解决方案1】:

写起来很有趣,很高兴看到其他解决方案,但无论如何这是我的尝试。

我在代码中添加了 cmets 而不是在这里描述方法,因为我认为 cmets 更容易理解...

// Test HTML
$startHTML = '<div wrap>1</div>
<div>2</div>
<div wrap>3</div>
<div wrap>4</div>
<div wrap>5</div>';

$doc = new DOMDocument();
$doc->loadHTML($startHTML);

$xp = new DOMXPath($doc);
// Find any div tag with a wrap attribute which doesn't have an immediately preceeding
// tag with a wrap attribute, (or the first node which means it won't have a preceeding
// element anyway)
$wrapList = $xp->query("//div[@wrap='' and preceding-sibling::*[1][not(@wrap)]
                           or position() = 1]");

// Iterate over each of the first in the list of wrapped nodes
foreach ( $wrapList as $wrap )  {
    // Create new wrapper 
    $wrapper = $doc->createElement("div");
    $class = $doc->createAttribute("class");
    $class->value = "wrapper";
    $wrapper->appendChild($class);

    // Copy subsequent wrap nodes (if any)
    $nextNode = $wrap->nextSibling;
    while ( $nextNode ) {
        $next = $nextNode;
        $nextNode = $nextNode->nextSibling;
        // If it's an element (and not a text node etc)
        if ( $next->nodeType == XML_ELEMENT_NODE ) {
            // If it also has a wrap attribute - copy it
            if ($next->hasAttribute("wrap") ) {
                $wrapper->appendChild($next);
            }
            // If no attribute, then finished copying
            else    {
                break;
            }
        }
    }
    // Replace first wrap node with new wrapper
    $wrap->parentNode->replaceChild($wrapper, $wrap);
    // Move the wrap node into the wrapper
    $wrapper->insertBefore($wrap, $wrapper->firstChild);
}
echo $doc->saveHTML();

由于它使用的是 HTML,最终结果也都包含在标准标签中,但输出(格式化)是......

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
    <body>
        <div class="wrapper">
            <div wrap>1</div>
        </div>
        <div>2</div>
        <div class="wrapper">
            <div wrap>3</div>
            <div wrap>4</div>
            <div wrap>5</div>
        </div>

    </body>
</html>

编辑:

如果您只希望它应用于 &lt;body&gt; 标记的直接后代,则更新 XPath 表达式以将其作为条件的一部分...

$wrapList = $xp->query("//body/div[@wrap='' and preceding-sibling::*[1][not(@wrap)]
                       or position() = 1]");

【讨论】:

  • 清晰、简洁、易读、易测试、易预测。谢谢楼主:)
  • 注意:虽然这段代码对我提供的输入工作正常,但请记住,它会将包装器添加到任何级别的 childNodes,而不仅仅是 body 的孩子。
  • 很难为通用解决方案编写代码,尽管我尝试测试各种组合,但无法捕捉所有情况。如果你需要一些特别的东西,我可以试试看。
  • 我试图将包装器限制在身体的孩子(直接后代)上,而不是比这更深的任何层次,但 xpath 并不是我真正的东西..你将如何构造查询来实现这一点?
  • 没问题 - 挑战自己也很有趣 - 您应该能够通过将 XPath 表达式更新为 //dody/div[@wra... 来做到这一点。所以你说 div 标签必须在 body 标签下一级。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-04-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-06
  • 2021-12-13
  • 1970-01-01
  • 2016-04-18
相关资源
最近更新 更多