Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix misaligned tags for overlapping marks #37

Merged
merged 3 commits into from
Jun 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 62 additions & 4 deletions src/Core/DOMSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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];
}
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
180 changes: 179 additions & 1 deletion tests/DOMSerializer/MultipleMarksTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,184 @@
];

$result = (new Editor)->setContent($document)->getHTML();

expect($result)->toEqual('<p><strong><em>Example Text</em></strong></p>');
});


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('<em><strong>lorem </strong></em><strong>ipsum</strong>');
});


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('<em>lorem <strong>ipsum</strong></em>');
});


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('<p><strong>lorem <em>ipsum</em></strong><em> dolor</em> sit</p>');
});


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('<p><strong><strike>lorem <em>ipsum</em></strike></strong><strike><em> dolor</em></strike></p>');
});