Added converter for lists
authorAlexander Ebert <ebert@woltlab.com>
Tue, 12 Jul 2016 08:22:39 +0000 (10:22 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 12 Jul 2016 08:22:51 +0000 (10:22 +0200)
wcfsetup/install/files/lib/system/html/metacode/converter/ListMetacodeConverter.class.php [new file with mode: 0644]

diff --git a/wcfsetup/install/files/lib/system/html/metacode/converter/ListMetacodeConverter.class.php b/wcfsetup/install/files/lib/system/html/metacode/converter/ListMetacodeConverter.class.php
new file mode 100644 (file)
index 0000000..56a30a4
--- /dev/null
@@ -0,0 +1,198 @@
+<?php
+namespace wcf\system\html\metacode\converter;
+use wcf\util\DOMUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Converts list bbcode into `<ol>`/`<ul>`.
+ * 
+ * @author      Alexander Ebert
+ * @copyright   2001-2016 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package     WoltLabSuite\Core\System\Html\Metacode\Converter
+ * @since       3.0
+ */
+class ListMetacodeConverter extends AbstractMetacodeConverter {
+       /**
+        * @inheritDoc
+        */
+       public function convert(\DOMDocumentFragment $fragment, array $attributes) {
+               $tagName = 'ul';
+               $listType = (!empty($attributes[0])) ? $attributes[0] : 'none';
+               if ($listType == 'a' || $listType == 'decimal') {
+                       $tagName = 'ol';
+               }
+               
+               $element = $fragment->ownerDocument->createElement($tagName);
+               $element->appendChild($fragment);
+               
+               // get all text nodes
+               $nodes = [];
+               $xpath = new \DOMXPath($element->ownerDocument);
+               /** @var \DOMText $node */
+               foreach ($xpath->query('.//text()', $element) as $node) {
+                       if (mb_strpos($node->textContent, '[*]') !== false && !$this->isInsideList($node)) {
+                               $nodes[] = $node;
+                       }
+               }
+               
+               // handle empty lists
+               if (empty($nodes)) {
+                       $element->appendChild($element->ownerDocument->createElement('li'));
+               }
+               else {
+                       $targetNodes = [];
+                       foreach ($nodes as $node) {
+                               $parts = preg_split('~(\[\*\])~', $node->textContent, null, PREG_SPLIT_DELIM_CAPTURE);
+                               $parent = $node->parentNode;
+                               foreach ($parts as $part) {
+                                       switch ($part) {
+                                               case '':
+                                                       // ignore
+                                                       break;
+                                               
+                                               case '[*]':
+                                                       $listItem = $parent->ownerDocument->createElement('woltlab-list-item');
+                                                       $parent->insertBefore($listItem, $node);
+                                                       $targetNodes[] = $listItem;
+                                                       break;
+                                               
+                                               default:
+                                                       $textNode = $parent->ownerDocument->createTextNode($part);
+                                                       $parent->insertBefore($textNode, $node);
+                                                       break;
+                                       }
+                               }
+                               
+                               $parent->removeChild($node);
+                       }
+                       
+                       // split before each target node
+                       foreach ($targetNodes as $targetNode) {
+                               DOMUtil::splitParentsUntil($targetNode, $element); 
+                       }
+                       $ancestors = [];
+                       foreach ($targetNodes as $targetNode) {
+                               $ancestors[] = DOMUtil::getParentBefore($targetNode, $element);
+                       }
+                       
+                       $childNodes = [];
+                       foreach ($element->childNodes as $childNode) $childNodes[] = $childNode;
+                       
+                       $listItem = $element->ownerDocument->createElement('li');
+                       for ($i = 0, $length = count($childNodes); $i < $length; $i++) {
+                               $childNode = $childNodes[$i];
+                               if (in_array($childNode, $ancestors, true) && $i !== 0) {
+                                       $element->appendChild($listItem);
+                                       
+                                       // only create a new list item if this isn't the first child node
+                                       $listItem = $element->ownerDocument->createElement('li');
+                               }
+                               
+                               $listItem->appendChild($childNode);
+                       }
+                       $element->appendChild($listItem);
+                       
+                       // remove marker elements
+                       $markers = $element->getElementsByTagName('woltlab-list-item');
+                       while ($markers->length) {
+                               /** @var \DOMElement $marker */
+                               $marker = $markers[0];
+                               $marker->parentNode->removeChild($marker);
+                       }
+                       
+                       $childNodes = [];
+                       foreach ($element->childNodes as $childNode) $childNodes[] = $childNode;
+                       
+                       // remove <p> and replace it with <br>
+                       /** @var \DOMElement $childNode */
+                       foreach ($childNodes as $childNode) {
+                               /** @var \DOMElement $node */
+                               foreach ($childNode->childNodes as $node) {
+                                       if ($node->nodeName === 'p') {
+                                               if ($node->childNodes->length && $node->parentNode->lastChild !== $node) {
+                                                       DOMUtil::insertAfter($node->ownerDocument->createElement('br'), $node);
+                                               }
+                                               
+                                               DOMUtil::removeNode($node, true);
+                                       }
+                               }
+                               
+                               // check for empty <li> only containing whitespace, this can be a result
+                               // from the usages of <p> wrapping [list]
+                               $isEmpty = true;
+                               foreach ($childNode->childNodes as $node) {
+                                       if ($node->nodeType === XML_TEXT_NODE) {
+                                               if (StringUtil::trim($node->textContent) !== '') {
+                                                       $isEmpty = false;
+                                                       break;
+                                               }
+                                       }
+                                       else if ($node->nodeType === XML_ELEMENT_NODE) {
+                                               $isEmpty = false;
+                                               break;
+                                       }
+                               }
+                               
+                               if ($isEmpty) {
+                                       DOMUtil::removeNode($childNode);
+                               }
+                       }
+                       
+                       // remove trailing whitespaces and <br> from list items
+                       foreach ($element->childNodes as $childNode) {
+                               $node = $childNode->lastChild;
+                               while ($node !== null) {
+                                       if ($node->nodeType === XML_TEXT_NODE) {
+                                               if (StringUtil::trim($node->textContent) !== '') {
+                                                       break;
+                                               }
+                                       }
+                                       else if ($node->nodeType === XML_ELEMENT_NODE) {
+                                               if ($node->nodeName === 'p') {
+                                                       if ($node->hasChildNodes()) {
+                                                               break;
+                                                       }
+                                               }
+                                               else if ($node->nodeName !== 'br') {
+                                                       break;
+                                               }
+                                       }
+                                       
+                                       $removeNode = $node;
+                                       $node = $node->previousSibling;
+                                       
+                                       DOMUtil::removeNode($removeNode);
+                               }
+                       }
+               }
+               
+               return $element;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function validateAttributes(array $attributes) {
+               return true;
+       }
+       
+       /**
+        * Returns true if provided node is within another list, prevents issues
+        * with nested lists handled in the wrong order.
+        * 
+        * @param       \DOMNode        $node           target node
+        * @return      boolean         true if provided node is within another list
+        */
+       protected function isInsideList(\DOMNode $node) {
+               /** @var \DOMElement $parent */
+               $parent = $node;
+               while ($parent = $parent->parentNode) {
+                       if ($parent->nodeName === 'woltlab-metacode' && $parent->getAttribute('data-name') === 'list') {
+                               return true;
+                       }
+               }
+               
+               return false;
+       }
+}