From 3180f5e544b910ae6955ec8b49fe1f4583180f58 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 17 May 2023 16:06:40 +0200 Subject: [PATCH 1/3] fix misaligned tags for overlapping marks --- src/Core/DOMSerializer.php | 66 +++++++- tests/DOMParser/MultipleMarksTest.php | 1 - tests/DOMSerializer/MultipleMarksTest.php | 180 +++++++++++++++++++++- 3 files changed, 241 insertions(+), 6 deletions(-) diff --git a/src/Core/DOMSerializer.php b/src/Core/DOMSerializer.php index 3a8a983..7bd78e5 100644 --- a/src/Core/DOMSerializer.php +++ b/src/Core/DOMSerializer.php @@ -17,9 +17,10 @@ public function __construct($schema) $this->schema = $schema; } - private function renderNode($node, $previousNode = null, $nextNode = null): string + private function renderNode($node, $previousNode = null, $nextNode = null, &$markStack = []): string { $html = []; + $markTagsToClose = []; if (isset($node->marks)) { foreach ($node->marks as $mark) { @@ -35,6 +36,8 @@ private function renderNode($node, $previousNode = null, $nextNode = null): stri } $html[] = $this->renderOpeningTag($renderClass, $mark); + # push recently created mark tag to the stack + $markStack[] = [$renderClass, $mark]; } } } @@ -57,11 +60,12 @@ private function renderNode($node, $previousNode = null, $nextNode = null): stri } // child nodes elseif (isset($node->content)) { + $nestedNodeMarkStack = []; foreach ($node->content as $index => $nestedNode) { $previousNestedNode = $node->content[$index - 1] ?? null; $nextNestedNode = $node->content[$index + 1] ?? null; - $html[] = $this->renderNode($nestedNode, $previousNestedNode, $nextNestedNode); + $html[] = $this->renderNode($nestedNode, $previousNestedNode, $nextNestedNode, $nestedNodeMarkStack); } } // renderText($node) @@ -92,14 +96,66 @@ private function renderNode($node, $previousNode = null, $nextNode = null): stri continue; } - $html[] = $this->renderClosingTag($extension->renderHTML($mark)); + # remember which mark tags to close + $markTagsToClose[] = [$extension, $mark]; } } + # close mark tags and reopen when necessary + $html = array_merge($html, $this->closeAndReopenTags($markTagsToClose, $markStack)); } return join($html); } + private function closeAndReopenTags(array $markTagsToClose, array &$markStack): array + { + $markTagsToReopen = []; + $closingTags = $this->closeMarkTags($markTagsToClose, $markStack, $markTagsToReopen); + $reopeningTags = $this->reopenMarkTags($markTagsToReopen, $markStack); + return array_merge($closingTags, $reopeningTags); + } + + private function closeMarkTags($markTagsToClose, &$markStack, &$markTagsToReopen): array + { + $html = []; + while(!empty($markTagsToClose)) + { + # close mark tag from the top of the stack + $markTag = array_pop($markStack); + $markExtension = $markTag[0]; + $mark = $markTag[1]; + $html[] = $this->renderClosingTag($markExtension->renderHTML( $mark )); + + # check if the last closed tag is overlapping and has to be reopened + if(count(array_filter($markTagsToClose, function($markToClose) use ($markExtension, $mark){ + return $markExtension == $markToClose[0] && $mark == $markToClose[1]; + })) == 0) + { + $markTagsToReopen[] = $markTag; + } + else { + # mark tag does not have to be reopened, but deleted from the 'to close' list + $markTagsToClose = array_udiff($markTagsToClose, [$markTag], function ($a1, $a2) { + return strcmp($a1[1]->type, $a2[1]->type);}); + } + } + return $html; + } + + private function reopenMarkTags($markTagsToReopen, &$markStack): array + { + $html = []; + # reopen the overlapping mark tags and push them to the stack + foreach(array_reverse($markTagsToReopen) as $markTagToOpen) + { + $renderClass = $markTagToOpen[0]; + $mark = $markTagToOpen[1]; + $html[] = $this->renderOpeningTag($renderClass, $mark); + $markStack[] = [$renderClass, $mark]; + } + return $html; + } + private function isMarkOrNode($markOrNode, $renderClass): bool { return isset($markOrNode->type) && $markOrNode->type === $renderClass::$name; @@ -331,11 +387,13 @@ public function process(array $value): string $content = is_array($this->document->content) ? $this->document->content : []; + $markStack = []; + foreach ($content as $index => $node) { $previousNode = $content[$index - 1] ?? null; $nextNode = $content[$index + 1] ?? null; - $html[] = $this->renderNode($node, $previousNode, $nextNode); + $html[] = $this->renderNode($node, $previousNode, $nextNode, $markStack); } return join($html); diff --git a/tests/DOMParser/MultipleMarksTest.php b/tests/DOMParser/MultipleMarksTest.php index cb84856..93aeaf8 100644 --- a/tests/DOMParser/MultipleMarksTest.php +++ b/tests/DOMParser/MultipleMarksTest.php @@ -4,7 +4,6 @@ test('multiple marks are rendered correctly', function () { $html = '

Example Text

'; - $result = (new Editor)->setContent($html)->getDocument(); expect($result)->toEqual([ diff --git a/tests/DOMSerializer/MultipleMarksTest.php b/tests/DOMSerializer/MultipleMarksTest.php index 05a2f7a..bb8833d 100644 --- a/tests/DOMSerializer/MultipleMarksTest.php +++ b/tests/DOMSerializer/MultipleMarksTest.php @@ -29,6 +29,184 @@ ]; $result = (new Editor)->setContent($document)->getHTML(); - expect($result)->toEqual('

Example Text

'); }); + + +test('multiple marks get rendered correctly, with additional mark at the first node', function () { + $document = [ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'text', + 'marks' => [ + [ + 'type' => 'italic', + ], + [ + 'type' => 'bold', + ], + ], + 'text' => 'lorem ', + ], + [ + 'type' => 'text', + 'marks' => [ + [ + 'type' => 'bold', + ], + ], + 'text' => 'ipsum', + ], + ], + ]; + $result = (new Editor)->setContent($document)->getHTML(); + + expect($result)->toEqual('lorem ipsum'); +}); + + +test('multiple marks get rendered correctly, with additional mark at the last node', function () { + $document = [ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'text', + 'marks' => [ + [ + 'type' => 'italic', + ], + ], + 'text' => 'lorem ', + ], + [ + 'type' => 'text', + 'marks' => [ + [ + 'type' => 'italic', + ], + [ + 'type' => 'bold', + ], + ], + 'text' => 'ipsum', + ], + ], + ]; + $result = (new Editor)->setContent($document)->getHTML(); + + expect($result)->toEqual('lorem ipsum'); +}); + + +test('multiple marks get rendered correctly, when overlapping marks exist', function () { + $document = [ + "type" => "doc", + "content" => [ + [ + "type" => "paragraph", + "content" => [ + [ + "type" => "text", + "marks" => [ + [ + "type" => "bold" + ] + ], + "text" => "lorem " + ], + [ + "type" => "text", + "marks" => [ + [ + "type" => "bold" + ], + [ + "type" => "italic" + ] + ], + "text" => "ipsum" + ], + [ + "type" => "text", + "marks" => [ + [ + "type" => "italic" + ] + ], + "text" => " dolor" + ], + [ + "type" => "text", + "text" => " sit" + ], + ] + ] + ] + ]; + + $result = (new Editor) + ->setContent($document) + ->getHTML(); + + expect($result)->toEqual('

lorem ipsum dolor sit

'); +}); + + +test('multiple marks get rendered correctly, when overlapping passage with multiple marks exist', function () { + $document = [ + "type" => "doc", + "content" => [ + [ + "type" => "paragraph", + "content" => [ + [ + "type" => "text", + "marks" => [ + [ + "type" => "bold" + ], + [ + "type" => "strike" + ] + ], + "text" => "lorem " + ], + [ + "type" => "text", + "marks" => [ + [ + "type" => "italic" + ], + [ + "type" => "bold" + ], + [ + "type" => "strike" + ] + ], + "text" => "ipsum" + ], + [ + "type" => "text", + "marks" => [ + [ + "type" => "strike" + ], + [ + "type" => "italic" + ], + ], + "text" => " dolor" + ] + ] + ] + ] + ]; + + $result = (new Editor) + ->setContent($document) + ->getHTML(); + + expect($result)->toEqual('

lorem ipsum dolor

'); +}); From 0d0b83b580bf33b7b036b51e1a727ffb453b7622 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 17 May 2023 17:47:38 +0200 Subject: [PATCH 2/3] revert mistaken change --- tests/DOMParser/MultipleMarksTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/DOMParser/MultipleMarksTest.php b/tests/DOMParser/MultipleMarksTest.php index 93aeaf8..cb84856 100644 --- a/tests/DOMParser/MultipleMarksTest.php +++ b/tests/DOMParser/MultipleMarksTest.php @@ -4,6 +4,7 @@ test('multiple marks are rendered correctly', function () { $html = '

Example Text

'; + $result = (new Editor)->setContent($html)->getDocument(); expect($result)->toEqual([ From 7c05d1277ae25b3dab19d8f1638ce8ea4c098aaa Mon Sep 17 00:00:00 2001 From: faltjo Date: Mon, 22 May 2023 08:11:22 +0000 Subject: [PATCH 3/3] Fix styling --- src/Core/DOMSerializer.php | 24 ++++----- tests/DOMSerializer/MultipleMarksTest.php | 60 +++++++++++------------ 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/Core/DOMSerializer.php b/src/Core/DOMSerializer.php index 7bd78e5..974401a 100644 --- a/src/Core/DOMSerializer.php +++ b/src/Core/DOMSerializer.php @@ -112,33 +112,33 @@ private function closeAndReopenTags(array $markTagsToClose, array &$markStack): $markTagsToReopen = []; $closingTags = $this->closeMarkTags($markTagsToClose, $markStack, $markTagsToReopen); $reopeningTags = $this->reopenMarkTags($markTagsToReopen, $markStack); + return array_merge($closingTags, $reopeningTags); } private function closeMarkTags($markTagsToClose, &$markStack, &$markTagsToReopen): array { $html = []; - while(!empty($markTagsToClose)) - { + while(! empty($markTagsToClose)) { # close mark tag from the top of the stack $markTag = array_pop($markStack); $markExtension = $markTag[0]; $mark = $markTag[1]; - $html[] = $this->renderClosingTag($markExtension->renderHTML( $mark )); + $html[] = $this->renderClosingTag($markExtension->renderHTML($mark)); # check if the last closed tag is overlapping and has to be reopened - if(count(array_filter($markTagsToClose, function($markToClose) use ($markExtension, $mark){ - return $markExtension == $markToClose[0] && $mark == $markToClose[1]; - })) == 0) - { + if(count(array_filter($markTagsToClose, function ($markToClose) use ($markExtension, $mark) { + return $markExtension == $markToClose[0] && $mark == $markToClose[1]; + })) == 0) { $markTagsToReopen[] = $markTag; - } - else { + } else { # mark tag does not have to be reopened, but deleted from the 'to close' list $markTagsToClose = array_udiff($markTagsToClose, [$markTag], function ($a1, $a2) { - return strcmp($a1[1]->type, $a2[1]->type);}); + return strcmp($a1[1]->type, $a2[1]->type); + }); } } + return $html; } @@ -146,13 +146,13 @@ private function reopenMarkTags($markTagsToReopen, &$markStack): array { $html = []; # reopen the overlapping mark tags and push them to the stack - foreach(array_reverse($markTagsToReopen) as $markTagToOpen) - { + foreach(array_reverse($markTagsToReopen) as $markTagToOpen) { $renderClass = $markTagToOpen[0]; $mark = $markTagToOpen[1]; $html[] = $this->renderOpeningTag($renderClass, $mark); $markStack[] = [$renderClass, $mark]; } + return $html; } diff --git a/tests/DOMSerializer/MultipleMarksTest.php b/tests/DOMSerializer/MultipleMarksTest.php index bb8833d..3111fe0 100644 --- a/tests/DOMSerializer/MultipleMarksTest.php +++ b/tests/DOMSerializer/MultipleMarksTest.php @@ -110,39 +110,39 @@ "type" => "text", "marks" => [ [ - "type" => "bold" - ] + "type" => "bold", + ], ], - "text" => "lorem " + "text" => "lorem ", ], [ "type" => "text", "marks" => [ [ - "type" => "bold" + "type" => "bold", ], [ - "type" => "italic" - ] + "type" => "italic", + ], ], - "text" => "ipsum" + "text" => "ipsum", ], [ "type" => "text", "marks" => [ [ - "type" => "italic" - ] + "type" => "italic", + ], ], - "text" => " dolor" + "text" => " dolor", ], [ "type" => "text", - "text" => " sit" + "text" => " sit", ], - ] - ] - ] + ], + ], + ], ]; $result = (new Editor) @@ -164,44 +164,44 @@ "type" => "text", "marks" => [ [ - "type" => "bold" + "type" => "bold", ], [ - "type" => "strike" - ] + "type" => "strike", + ], ], - "text" => "lorem " + "text" => "lorem ", ], [ "type" => "text", "marks" => [ [ - "type" => "italic" + "type" => "italic", ], [ - "type" => "bold" + "type" => "bold", ], [ - "type" => "strike" - ] + "type" => "strike", + ], ], - "text" => "ipsum" + "text" => "ipsum", ], [ "type" => "text", "marks" => [ [ - "type" => "strike" + "type" => "strike", ], [ - "type" => "italic" + "type" => "italic", ], ], - "text" => " dolor" - ] - ] - ] - ] + "text" => " dolor", + ], + ], + ], + ], ]; $result = (new Editor)