Skip to content

Commit 1d4be93

Browse files
authored
Merge pull request #37 from faltjo/fix-overlapping-marks
fix misaligned tags for overlapping marks
2 parents ff25575 + 7c05d12 commit 1d4be93

File tree

2 files changed

+241
-5
lines changed

2 files changed

+241
-5
lines changed

src/Core/DOMSerializer.php

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ public function __construct($schema)
1717
$this->schema = $schema;
1818
}
1919

20-
private function renderNode($node, $previousNode = null, $nextNode = null): string
20+
private function renderNode($node, $previousNode = null, $nextNode = null, &$markStack = []): string
2121
{
2222
$html = [];
23+
$markTagsToClose = [];
2324

2425
if (isset($node->marks)) {
2526
foreach ($node->marks as $mark) {
@@ -35,6 +36,8 @@ private function renderNode($node, $previousNode = null, $nextNode = null): stri
3536
}
3637

3738
$html[] = $this->renderOpeningTag($renderClass, $mark);
39+
# push recently created mark tag to the stack
40+
$markStack[] = [$renderClass, $mark];
3841
}
3942
}
4043
}
@@ -57,11 +60,12 @@ private function renderNode($node, $previousNode = null, $nextNode = null): stri
5760
}
5861
// child nodes
5962
elseif (isset($node->content)) {
63+
$nestedNodeMarkStack = [];
6064
foreach ($node->content as $index => $nestedNode) {
6165
$previousNestedNode = $node->content[$index - 1] ?? null;
6266
$nextNestedNode = $node->content[$index + 1] ?? null;
6367

64-
$html[] = $this->renderNode($nestedNode, $previousNestedNode, $nextNestedNode);
68+
$html[] = $this->renderNode($nestedNode, $previousNestedNode, $nextNestedNode, $nestedNodeMarkStack);
6569
}
6670
}
6771
// renderText($node)
@@ -92,14 +96,66 @@ private function renderNode($node, $previousNode = null, $nextNode = null): stri
9296
continue;
9397
}
9498

95-
$html[] = $this->renderClosingTag($extension->renderHTML($mark));
99+
# remember which mark tags to close
100+
$markTagsToClose[] = [$extension, $mark];
96101
}
97102
}
103+
# close mark tags and reopen when necessary
104+
$html = array_merge($html, $this->closeAndReopenTags($markTagsToClose, $markStack));
98105
}
99106

100107
return join($html);
101108
}
102109

110+
private function closeAndReopenTags(array $markTagsToClose, array &$markStack): array
111+
{
112+
$markTagsToReopen = [];
113+
$closingTags = $this->closeMarkTags($markTagsToClose, $markStack, $markTagsToReopen);
114+
$reopeningTags = $this->reopenMarkTags($markTagsToReopen, $markStack);
115+
116+
return array_merge($closingTags, $reopeningTags);
117+
}
118+
119+
private function closeMarkTags($markTagsToClose, &$markStack, &$markTagsToReopen): array
120+
{
121+
$html = [];
122+
while(! empty($markTagsToClose)) {
123+
# close mark tag from the top of the stack
124+
$markTag = array_pop($markStack);
125+
$markExtension = $markTag[0];
126+
$mark = $markTag[1];
127+
$html[] = $this->renderClosingTag($markExtension->renderHTML($mark));
128+
129+
# check if the last closed tag is overlapping and has to be reopened
130+
if(count(array_filter($markTagsToClose, function ($markToClose) use ($markExtension, $mark) {
131+
return $markExtension == $markToClose[0] && $mark == $markToClose[1];
132+
})) == 0) {
133+
$markTagsToReopen[] = $markTag;
134+
} else {
135+
# mark tag does not have to be reopened, but deleted from the 'to close' list
136+
$markTagsToClose = array_udiff($markTagsToClose, [$markTag], function ($a1, $a2) {
137+
return strcmp($a1[1]->type, $a2[1]->type);
138+
});
139+
}
140+
}
141+
142+
return $html;
143+
}
144+
145+
private function reopenMarkTags($markTagsToReopen, &$markStack): array
146+
{
147+
$html = [];
148+
# reopen the overlapping mark tags and push them to the stack
149+
foreach(array_reverse($markTagsToReopen) as $markTagToOpen) {
150+
$renderClass = $markTagToOpen[0];
151+
$mark = $markTagToOpen[1];
152+
$html[] = $this->renderOpeningTag($renderClass, $mark);
153+
$markStack[] = [$renderClass, $mark];
154+
}
155+
156+
return $html;
157+
}
158+
103159
private function isMarkOrNode($markOrNode, $renderClass): bool
104160
{
105161
return isset($markOrNode->type) && $markOrNode->type === $renderClass::$name;
@@ -331,11 +387,13 @@ public function process(array $value): string
331387

332388
$content = is_array($this->document->content) ? $this->document->content : [];
333389

390+
$markStack = [];
391+
334392
foreach ($content as $index => $node) {
335393
$previousNode = $content[$index - 1] ?? null;
336394
$nextNode = $content[$index + 1] ?? null;
337395

338-
$html[] = $this->renderNode($node, $previousNode, $nextNode);
396+
$html[] = $this->renderNode($node, $previousNode, $nextNode, $markStack);
339397
}
340398

341399
return join($html);

tests/DOMSerializer/MultipleMarksTest.php

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,184 @@
2929
];
3030

3131
$result = (new Editor)->setContent($document)->getHTML();
32-
3332
expect($result)->toEqual('<p><strong><em>Example Text</em></strong></p>');
3433
});
34+
35+
36+
test('multiple marks get rendered correctly, with additional mark at the first node', function () {
37+
$document = [
38+
'type' => 'doc',
39+
'content' => [
40+
[
41+
'type' => 'text',
42+
'marks' => [
43+
[
44+
'type' => 'italic',
45+
],
46+
[
47+
'type' => 'bold',
48+
],
49+
],
50+
'text' => 'lorem ',
51+
],
52+
[
53+
'type' => 'text',
54+
'marks' => [
55+
[
56+
'type' => 'bold',
57+
],
58+
],
59+
'text' => 'ipsum',
60+
],
61+
],
62+
];
63+
$result = (new Editor)->setContent($document)->getHTML();
64+
65+
expect($result)->toEqual('<em><strong>lorem </strong></em><strong>ipsum</strong>');
66+
});
67+
68+
69+
test('multiple marks get rendered correctly, with additional mark at the last node', function () {
70+
$document = [
71+
'type' => 'doc',
72+
'content' => [
73+
[
74+
'type' => 'text',
75+
'marks' => [
76+
[
77+
'type' => 'italic',
78+
],
79+
],
80+
'text' => 'lorem ',
81+
],
82+
[
83+
'type' => 'text',
84+
'marks' => [
85+
[
86+
'type' => 'italic',
87+
],
88+
[
89+
'type' => 'bold',
90+
],
91+
],
92+
'text' => 'ipsum',
93+
],
94+
],
95+
];
96+
$result = (new Editor)->setContent($document)->getHTML();
97+
98+
expect($result)->toEqual('<em>lorem <strong>ipsum</strong></em>');
99+
});
100+
101+
102+
test('multiple marks get rendered correctly, when overlapping marks exist', function () {
103+
$document = [
104+
"type" => "doc",
105+
"content" => [
106+
[
107+
"type" => "paragraph",
108+
"content" => [
109+
[
110+
"type" => "text",
111+
"marks" => [
112+
[
113+
"type" => "bold",
114+
],
115+
],
116+
"text" => "lorem ",
117+
],
118+
[
119+
"type" => "text",
120+
"marks" => [
121+
[
122+
"type" => "bold",
123+
],
124+
[
125+
"type" => "italic",
126+
],
127+
],
128+
"text" => "ipsum",
129+
],
130+
[
131+
"type" => "text",
132+
"marks" => [
133+
[
134+
"type" => "italic",
135+
],
136+
],
137+
"text" => " dolor",
138+
],
139+
[
140+
"type" => "text",
141+
"text" => " sit",
142+
],
143+
],
144+
],
145+
],
146+
];
147+
148+
$result = (new Editor)
149+
->setContent($document)
150+
->getHTML();
151+
152+
expect($result)->toEqual('<p><strong>lorem <em>ipsum</em></strong><em> dolor</em> sit</p>');
153+
});
154+
155+
156+
test('multiple marks get rendered correctly, when overlapping passage with multiple marks exist', function () {
157+
$document = [
158+
"type" => "doc",
159+
"content" => [
160+
[
161+
"type" => "paragraph",
162+
"content" => [
163+
[
164+
"type" => "text",
165+
"marks" => [
166+
[
167+
"type" => "bold",
168+
],
169+
[
170+
"type" => "strike",
171+
],
172+
],
173+
"text" => "lorem ",
174+
],
175+
[
176+
"type" => "text",
177+
"marks" => [
178+
[
179+
"type" => "italic",
180+
],
181+
[
182+
"type" => "bold",
183+
],
184+
[
185+
"type" => "strike",
186+
],
187+
],
188+
"text" => "ipsum",
189+
],
190+
[
191+
"type" => "text",
192+
"marks" => [
193+
[
194+
"type" => "strike",
195+
],
196+
[
197+
"type" => "italic",
198+
],
199+
],
200+
"text" => " dolor",
201+
],
202+
],
203+
],
204+
],
205+
];
206+
207+
$result = (new Editor)
208+
->setContent($document)
209+
->getHTML();
210+
211+
expect($result)->toEqual('<p><strong><strike>lorem <em>ipsum</em></strike></strong><strike><em> dolor</em></strike></p>');
212+
});

0 commit comments

Comments
 (0)