Repository: springload/draftjs_exporter

Issue:
Entities with adjacent offset are rendered incorrectly
Test version: v2.1.4

Test with the following code:

```python3
from draftjs_exporter.constants import BLOCK_TYPES
from draftjs_exporter.constants import ENTITY_TYPES
from draftjs_exporter.defaults import BLOCK_MAP
from draftjs_exporter.dom import DOM
from draftjs_exporter.html import HTML
import json

a = '''{
    "blocks": [
        {
            "key": "bh6r4",
            "text": "🙄😖",
            "type": "unstyled",
            "depth": 0,
            "inlineStyleRanges": [],
            "entityRanges": [
                {
                "offset": 0,
                "length": 1,
                "key": 7
                },
                {
                "offset": 1,
                "length": 1,
                "key": 8
                }
            ],
            "data": {}
        }
    ],
    "entityMap": {
        "7": {
            "type": "emoji",
            "mutability": "IMMUTABLE",
            "data": {
                "emojiUnicode": "🙄"
            }
        },
        "8": {
            "type": "emoji",
            "mutability": "IMMUTABLE",
            "data": {
                "emojiUnicode": "😖"
            }
        }
    }
}'''


def emoji(props):
    emoji_encode = []
    for c in props.get('emojiUnicode'):
        code = '%04x' % ord(c)
        if code != '200d':
            emoji_encode.append('%04x' % ord(c))

    return DOM.create_element('span', {
        'data-emoji': '-'.join(emoji_encode),
        'class': 'emoji',
    }, props['children'])


def entity_fallback(props):
    return DOM.create_element('span', {'class': 'missing-entity'},
                              props['children'])


def style_fallback(props):
    return props['children']


def block_fallback(props):
    return DOM.create_element('div', {}, props['children'])


DRAFTJS_EXPORTER_CONFIG = {
    'entity_decorators': {
        'emoji': emoji,
        ENTITY_TYPES.FALLBACK: entity_fallback,
    },
    'block_map': dict(BLOCK_MAP, **{
        BLOCK_TYPES.FALLBACK: block_fallback,
    })
}

exporter = HTML(DRAFTJS_EXPORTER_CONFIG)

if __name__ == '__main__':
    print(exporter.render(json.loads(a)))
```

Actual output:

```html
<p><span class="emoji" data-emoji="1f644">🙄</span>😖<span class="emoji" data-emoji="1f616"></span></p>
```

Expected output:

```html
<p><span class="emoji" data-emoji="1f644">🙄</span><span class="emoji" data-emoji="1f616">😖</span></p>
```

Discussion:
thibaudcolas: 👌 I have added the fix alongside the tests.

Patch:
diff --git a/draftjs_exporter/entity_state.py b/draftjs_exporter/entity_state.py
index bffd6f8..4b10dcb 100644
--- a/draftjs_exporter/entity_state.py
+++ b/draftjs_exporter/entity_state.py
@@ -29,6 +29,9 @@ class EntityState:
 
             self.completed_entity = self.entity_stack.pop()
 
+    def has_entity(self):
+        return self.entity_stack
+
     def has_no_entity(self):
         return not self.entity_stack
 
@@ -41,7 +44,7 @@ class EntityState:
         return details
 
     def render_entities(self, style_node):
-
+        # We have a complete (start, stop) entity to render.
         if self.completed_entity is not None:
             entity_details = self.get_entity_details(self.completed_entity)
             opts = Options.for_entity(self.entity_decorators, entity_details['type'])
@@ -55,14 +58,17 @@ class EntityState:
             for n in self.element_stack:
                 DOM.append_child(nodes, n)
 
-            elt = DOM.create_element(opts.element, props, nodes)
-
             self.completed_entity = None
             self.element_stack = []
-        elif self.has_no_entity():
-            elt = style_node
-        else:
+
+            # Is there still another entity? (adjacent) if so add the current style_node for it.
+            if self.has_entity():
+                self.element_stack.append(style_node)
+
+            return DOM.create_element(opts.element, props, nodes)
+
+        if self.has_entity():
             self.element_stack.append(style_node)
-            elt = None
+            return None
 
-        return elt
+        return style_node
diff --git a/draftjs_exporter/html.py b/draftjs_exporter/html.py
index 5267bd7..347c237 100644
--- a/draftjs_exporter/html.py
+++ b/draftjs_exporter/html.py
@@ -80,7 +80,9 @@ class HTML:
 
                 if entity_node is not None:
                     DOM.append_child(content, entity_node)
-                    if styled_node != entity_node:
+
+                    # Check whether there actually are two different nodes, confirming we are not inserting an upcoming entity.
+                    if styled_node != entity_node and entity_state.has_no_entity():
                         DOM.append_child(content, styled_node)
         # Fast track for blocks which do not contain styles nor entities, which is very common.
         else:


Test patch:
diff --git a/tests/test_exports.json b/tests/test_exports.json
index 088caa1..910abfd 100644
--- a/tests/test_exports.json
+++ b/tests/test_exports.json
@@ -148,6 +148,55 @@
         }
     },
 
+    {
+        "label": "Adjacent entities",
+        "output": {
+            "html5lib": "<p><a href=\"https://google.com\">G</a><a href=\"https://facebook.com\">F</a></p>",
+            "lxml": "<p><a href=\"https://google.com\">G</a><a href=\"https://facebook.com\">F</a></p>",
+            "string": "<p><a href=\"https://google.com\">G</a><a href=\"https://facebook.com\">F</a></p>"
+        },
+        "content_state": {
+            "blocks": [
+                {
+                    "key": "9nc73",
+                    "text": "GF",
+                    "type": "unstyled",
+                    "depth": 0,
+                    "inlineStyleRanges": [],
+                    "entityRanges": [
+                        {
+                        "offset": 0,
+                        "length": 1,
+                        "key": 7
+                        },
+                        {
+                        "offset": 1,
+                        "length": 1,
+                        "key": 8
+                        }
+                    ],
+                    "data": {}
+                }
+            ],
+            "entityMap": {
+                "7": {
+                    "type": "LINK",
+                    "mutability": "MUTABLE",
+                    "data": {
+                        "url": "https://google.com"
+                    }
+                },
+                "8": {
+                    "type": "LINK",
+                    "mutability": "MUTABLE",
+                    "data": {
+                        "url": "https://facebook.com"
+                    }
+                }
+            }
+        }
+    },
+
     {
         "label": "Style map defaults",
         "output": {