--- /dev/null
+<?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;
+ }
+}