Unwrap and strip trailing `<br>`
authorAlexander Ebert <ebert@woltlab.com>
Thu, 4 May 2023 19:10:27 +0000 (21:10 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 8 May 2023 16:34:04 +0000 (18:34 +0200)
wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeBr.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/util/DOMUtil.class.php

diff --git a/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeBr.class.php b/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeBr.class.php
new file mode 100644 (file)
index 0000000..2afc6be
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+
+namespace wcf\system\html\output\node;
+
+use DOMElement;
+use wcf\system\html\node\AbstractHtmlNodeProcessor;
+use wcf\util\DOMUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Unwraps <br> and strips trailing <br>.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2023 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ */
+final class HtmlOutputNodeBr extends AbstractHtmlOutputNode
+{
+    /**
+     * @inheritDoc
+     */
+    protected $tagName = 'br';
+
+    /**
+     * @inheritDoc
+     */
+    public function process(array $elements, AbstractHtmlNodeProcessor $htmlNodeProcessor)
+    {
+        /** @var \DOMElement $element */
+        foreach ($elements as $element) {
+            $this->unwrap($element);
+            $this->removeTrailingBr($element);
+        }
+    }
+
+    private function unwrap(DOMElement $br): void
+    {
+        if ($br->previousSibling || $br->nextSibling) {
+            return;
+        }
+
+        $parent = $br;
+        while (($parent = $parent->parentNode) !== null) {
+            switch ($parent->nodeName) {
+                case "b":
+                case "em":
+                case "i":
+                case "strong":
+                case "sub":
+                case "sup":
+                case "span":
+                case "u":
+                    if ($br->previousSibling || $br->nextSibling) {
+                        return;
+                    }
+
+                    $parent->parentNode->insertBefore($br, $parent);
+                    $parent->parentNode->removeChild($parent);
+                    $parent = $br;
+
+                    break;
+
+                default:
+                    return;
+            }
+        }
+    }
+
+    private function removeTrailingBr(DOMElement $br): void
+    {
+        if ($br->getAttribute("data-cke-filler") === "true") {
+            return;
+        }
+
+        $paragraph = DOMUtil::closest($br, "p");
+        if ($paragraph === null) {
+            return;
+        }
+
+        if (!DOMUtil::isLastNode($br, $paragraph)) {
+            return;
+        }
+
+        if ($paragraph->childNodes->length === 1 && $paragraph->childNodes->item(0) === $br) {
+            $paragraph->parentNode->removeChild($paragraph);
+        } else {
+            $br->remove();
+        }
+    }
+}
index cbda410b58767d3f289c5490760478227b9d3c45..f730bad6686e1af55956e7ae5d0194906dd422a9 100644 (file)
@@ -232,6 +232,22 @@ final class DOMUtil
         return false;
     }
 
+    /**
+     * Finds the closest parent matching the tag name.
+     *
+     * @since 6.0
+     */
+    public static function closest(\DOMNode $node, string $tagName): \DOMNode|null
+    {
+        while ($node = $node->parentNode) {
+            if ($node->nodeName === $tagName) {
+                return $node;
+            }
+        }
+
+        return null;
+    }
+
     /**
      * Inserts given DOM node after the reference node.
      */