2 namespace wcf\system\html\input\node
;
3 use wcf\system\bbcode\HtmlBBCodeParser
;
4 use wcf\system\html\node\AbstractHtmlNodeProcessor
;
8 * Transforms bbcode markers into the custom HTML element `<woltlab-metacode>`. This process
9 * outputs well-formed markup with proper element nesting.
11 * @author Alexander Ebert
12 * @copyright 2001-2016 WoltLab GmbH
13 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
14 * @package WoltLabSuite\Core\System\Html\Input\Node
17 class HtmlInputNodeWoltlabMetacodeMarker
extends AbstractHtmlInputNode
{
19 * list of bbcodes that represent block elements
22 public $blockElements = [];
25 * list of bbcodes that represent source code elements
28 public $sourceElements = [];
33 protected $tagName = 'woltlab-metacode-marker';
36 * HtmlInputNodeWoltlabMetacodeMarker constructor.
38 public function __construct() {
39 $this->blockElements
= HtmlBBCodeParser
::getInstance()->getBlockBBCodes();
40 $this->sourceElements
= HtmlBBCodeParser
::getInstance()->getSourceBBCodes();
46 public function isAllowed(AbstractHtmlNodeProcessor
$htmlNodeProcessor) {
47 // metacode-marker isn't present at time of validation
54 public function process(array $elements, AbstractHtmlNodeProcessor
$htmlNodeProcessor) {
56 $pairs = $this->buildPairs($elements);
58 // validate pairs and remove items that lack an opening/closing element
59 $pairs = $this->validatePairs($pairs);
61 // group pairs by tag name
62 $groups = $this->groupPairsByName($pairs);
64 // convert source bbcode groups first to ensure no bbcodes inside
65 // source blocks will be evaluated
66 $groups = $this->convertSourceGroups($groups);
68 $groups = $this->revertMarkerInsideCodeBlocks($groups, $htmlNodeProcessor);
70 // convert pairs into HTML or metacode
71 $this->convertGroups($groups);
75 * Transforms bbcode markers inside source code elements into their plain bbcode representation.
77 * @param array $groups grouped list of bbcode marker pairs
78 * @param AbstractHtmlNodeProcessor $htmlNodeProcessor node processor instance
79 * @return array filtered groups without source bbcodes
81 protected function revertMarkerInsideCodeBlocks(array $groups, AbstractHtmlNodeProcessor
$htmlNodeProcessor) {
82 $isInsideCode = function(\DOMElement
$element) {
84 while ($parent = $parent->parentNode
) {
85 $nodeName = $parent->nodeName
;
87 if ($nodeName === 'code' ||
$nodeName === 'kbd' ||
$nodeName === 'pre') {
90 else if ($nodeName === 'woltlab-metacode') {
91 $name = $parent->getAttribute('data-name');
92 if ($name === 'code' ||
$name === 'tt') {
101 foreach ($groups as $name => $pairs) {
102 $needsReindex = false;
103 for ($i = 0, $length = count($pairs); $i < $length; $i++
) {
105 if ($isInsideCode($pair['open']) ||
$isInsideCode($pair['close'])) {
106 $pair['attributes'] = $htmlNodeProcessor->parseAttributes($pair['attributes']);
107 $this->convertToBBCode($name, $pair);
109 $needsReindex = true;
110 unset($groups[$name][$i]);
112 if (empty($groups[$name])) {
113 $needsReindex = false;
114 unset($groups[$name]);
120 $groups[$name] = array_values($groups[$name]);
128 * Builds the list of paired bbcode markers.
130 * @param \DOMElement[] $elements list of marker elements
131 * @return array list of paired bbcode markers
133 protected function buildPairs(array $elements) {
135 /** @var \DOMElement $element */
136 foreach ($elements as $element) {
137 $attributes = $element->getAttribute('data-attributes');
138 $name = $element->getAttribute('data-name');
139 $uuid = $element->getAttribute('data-uuid');
140 $source = @base64_decode
($element->getAttribute('data-source'));
142 if (!isset($pairs[$uuid])) {
152 $pairs[$uuid]['attributes'] = $attributes;
153 $pairs[$uuid]['name'] = $name;
154 $pairs[$uuid]['open'] = $element;
155 $pairs[$uuid]['openSource'] = $source;
158 $pairs[$uuid]['close'] = $element;
159 $pairs[$uuid]['closeSource'] = $source;
167 * Validates bbcode marker pairs to include both an opening and closing element.
169 * @param array $pairs list of paired bbcode markers
170 * @return array filtered list of paired bbcode markers
172 protected function validatePairs(array $pairs) {
173 foreach ($pairs as $uuid => $data) {
174 if ($data['close'] === null) {
175 DOMUtil
::removeNode($data['open']);
177 else if ($data['open'] === null) {
178 DOMUtil
::removeNode($data['close']);
184 unset($pairs[$uuid]);
191 * Groups bbcode marker pairs by their common bbcode identifier.
193 * @param array $pairs list of paired bbcode markers
194 * @return array grouped list of bbcode marker pairs
196 protected function groupPairsByName(array $pairs) {
198 foreach ($pairs as $uuid => $data) {
199 $name = $data['name'];
201 if (!isset($groups[$name])) {
206 'attributes' => $data['attributes'],
207 'close' => $data['close'],
208 'closeSource' => $data['closeSource'],
209 'open' => $data['open'],
210 'openSource' => $data['openSource']
218 * Converts source bbcode groups.
220 * @param array $groups grouped list of bbcode marker pairs
221 * @return array filtered groups without source bbcodes
223 protected function convertSourceGroups(array $groups) {
224 foreach ($this->sourceElements
as $name) {
225 if (in_array($name, $this->blockElements
)) {
226 if (isset($groups[$name])) {
227 for ($i = 0, $length = count($groups[$name]); $i < $length; $i++
) {
228 $data = $groups[$name][$i];
229 $this->convertBlockElement($name, $data['open'], $data['close'], $data['attributes']);
232 unset($groups[$name]);
236 if (isset($groups[$name])) {
237 for ($i = 0, $length = count($groups[$name]); $i < $length; $i++
) {
238 $data = $groups[$name][$i];
239 $this->convertInlineElement($name, $data['open'], $data['close'], $data['attributes']);
242 unset($groups[$name]);
251 * Converts bbcode marker pairs into block- or inline-elements.
253 * @param array $groups grouped list of bbcode marker pairs
255 protected function convertGroups(array $groups) {
256 foreach ($this->blockElements
as $name) {
257 if (isset($groups[$name])) {
258 for ($i = 0, $length = count($groups[$name]); $i < $length; $i++
) {
259 $data = $groups[$name][$i];
260 $this->convertBlockElement($name, $data['open'], $data['close'], $data['attributes']);
263 unset($groups[$name]);
267 // treat remaining elements as inline elements
268 foreach ($groups as $name => $pairs) {
269 for ($i = 0, $length = count($pairs); $i < $length; $i++
) {
271 $this->convertInlineElement($name, $data['open'], $data['close'], $data['attributes']);
277 * Converts a block-level bbcode marker pair.
279 * @param string $name bbcode identifier
280 * @param \DOMElement $start start node
281 * @param \DOMElement $end end node
282 * @param string $attributes encoded attribute string
284 protected function convertBlockElement($name, $start, $end, $attributes) {
285 // we need to ensure proper nesting, block elements are not allowed to
286 // be placed inside paragraphs, but being a direct child of another block
287 // element is completely fine
290 $parent = $parent->parentNode
;
292 while ($parent->nodeName
=== 'p' ||
!$this->isBlockElement($parent));
294 $element = DOMUtil
::splitParentsUntil($start, $parent);
295 DOMUtil
::insertBefore($start, $element);
297 $commonAncestor = DOMUtil
::getCommonAncestor($start, $end);
298 $lastElement = DOMUtil
::splitParentsUntil($end, $commonAncestor, false);
300 $container = $start->ownerDocument
->createElement('woltlab-metacode');
301 $container->setAttribute('data-name', $name);
302 $container->setAttribute('data-attributes', $attributes);
304 DOMUtil
::insertAfter($container, $start);
305 DOMUtil
::removeNode($start);
307 DOMUtil
::moveNodesInto($container, $lastElement, $commonAncestor);
309 DOMUtil
::removeNode($end);
313 * Converts an inline bbcode marker pair.
315 * @param string $name bbcode identifier
316 * @param \DOMElement $start start node
317 * @param \DOMElement $end end node
318 * @param string $attributes encoded attribute string
320 protected function convertInlineElement($name, $start, $end, $attributes) {
321 if ($start->parentNode
=== $end->parentNode
) {
322 $this->wrapContent($name, $attributes, $start, $end);
324 DOMUtil
::removeNode($start);
325 DOMUtil
::removeNode($end);
328 $commonAncestor = DOMUtil
::getCommonAncestor($start, $end);
329 $endAncestor = DOMUtil
::getParentBefore($end, $commonAncestor);
331 $element = $this->wrapContent($name, $attributes, $start, null);
332 DOMUtil
::removeNode($start);
334 $element = DOMUtil
::getParentBefore($element, $commonAncestor);
335 while ($element = $element->nextSibling
) {
336 if ($element->nodeType
=== XML_TEXT_NODE
) {
337 // ignore text nodes between tags
341 if ($element !== $endAncestor) {
342 if ($this->isBlockElement($element)) {
343 $this->wrapContent($name, $attributes, $element->firstChild
, null);
346 $this->wrapContent($name, $attributes, $element, null);
350 $this->wrapContent($name, $attributes, null, $end);
352 DOMUtil
::removeNode($end);
360 * Wraps a sequence of nodes using a newly created element. If `$startNode` is `null` the end
361 * node and all previous siblings will be added to the element. The reverse takes place if
362 * `$endNode` is `null`.
364 * @param string $name element tag name
365 * @param string $attributes encoded attribute string
366 * @param \DOMElement|null $startNode first node to wrap
367 * @param \DOMElement|null $endNode last node to wrap
368 * @return \DOMElement newly created element
370 protected function wrapContent($name, $attributes, $startNode, $endNode) {
371 if ($startNode === null && $endNode === null) {
372 throw new \
InvalidArgumentException("Must provide an existing element for start node or end node, both cannot be null.");
375 $element = ($startNode) ?
$startNode->ownerDocument
->createElement('woltlab-metacode') : $endNode->ownerDocument
->createElement('woltlab-metacode');
376 $element->setAttribute('data-name', $name);
377 $element->setAttribute('data-attributes', $attributes);
380 DOMUtil
::insertBefore($element, $startNode);
382 while ($sibling = $element->nextSibling
) {
383 $element->appendChild($sibling);
385 if ($sibling === $endNode) {
391 DOMUtil
::insertAfter($element, $endNode);
393 while ($sibling = $element->previousSibling
) {
394 DOMUtil
::prepend($sibling, $element);
396 if ($sibling === $startNode) {
406 * Returns true if provided node is a block element.
408 * @param \DOMNode $node node
409 * @return boolean true for certain block elements
411 protected function isBlockElement(\DOMNode
$node) {
412 switch ($node->nodeName
) {
421 case 'woltlab-metacode':
422 /** @var \DOMElement $node */
423 if (in_array($node->getAttribute('data-name'), $this->blockElements
)) {
433 * Converts a bbcode marker pair into their plain bbcode representation. This method is used
434 * to convert markers inside source code elements.
436 * @param string $name bbcode name
437 * @param array $pair bbcode marker pair
439 protected function convertToBBCode($name, array $pair) {
440 /** @var \DOMElement $start */
441 $start = $pair['open'];
442 /** @var \DOMElement $end */
443 $end = $pair['close'];
445 $attributes = (isset($pair['attributes'])) ?
$pair['attributes'] : '';
446 $textNode = $start->ownerDocument
->createTextNode(($pair['openSource']) ?
: HtmlBBCodeParser
::getInstance()->buildBBCodeTag($name, $attributes, true));
447 DOMUtil
::insertBefore($textNode, $start);
448 DOMUtil
::removeNode($start);
450 $textNode = $end->ownerDocument
->createTextNode(($pair['closeSource']) ?
: '[/' . $name . ']');
451 DOMUtil
::insertBefore($textNode, $end);
452 DOMUtil
::removeNode($end);