Repository: springload/draftjs_exporter

Issue:
Remove support for class based decorator
Capturing offline discussions for a public discussion.

Currently a decorator can be written as a `function` (accepting a single positional argument `props`) or a `class` (with a single `render` method accepting a single positional argument `props`) which has a few issues:
- Nothing is passed to the `__init__` of the `class` but instead everything is passed to `props` which makes the `class` quite useless. Moving the props to the `__init__` will either create  boilerplate or will force the user to inherit from a custom class which does that for them. In short, it's not clear which benefit there is to use a class.
- To keep naming consistent, functions are names are camel cased (so when you update your config file, you don't have to think whether it's a function or a class) which gets the linter to complain. The library should not encourage non-PEP8 compliant code.

We were therefore thinking of removing support for class based decorators. Any thoughts?

Discussion:
thibaudcolas: @loicteixeira up

Patch:
diff --git a/README.rst b/README.rst
index b91b22c..94898d6 100644
--- a/README.rst
+++ b/README.rst
@@ -105,13 +105,13 @@ The exporter output is extensively configurable to cater for varied rich text re
                 'wrapper_props': {'class': 'bullet-list'},
             },
             # Use a custom component for more flexibility (reading block data or depth).
-            BLOCK_TYPES.BLOCKQUOTE: Blockquote,
+            BLOCK_TYPES.BLOCKQUOTE: blockquote,
             BLOCK_TYPES.ORDERED_LIST_ITEM: {
-                'element': ListItem,
-                'wrapper': OrderedList,
+                'element': list_item,
+                'wrapper': ordered_list,
             },
             # Provide a fallback component (advanced).
-            BLOCK_TYPES.FALLBACK: BlockFallback
+            BLOCK_TYPES.FALLBACK: block_fallback
         }),
         # `style_map` defines the HTML representation of inline elements.
         # Extend STYLE_MAP to start with sane defaults, or make your own from scratch.
@@ -123,25 +123,33 @@ The exporter output is extensively configurable to cater for varied rich text re
         }),
         'entity_decorators': {
             # Map entities to components so they can be rendered with their data.
-            ENTITY_TYPES.IMAGE: Image,
-            # Components can be defined as classes to receive extra parameters.
-            ENTITY_TYPES.LINK: Link(use_new_window=True),
+            ENTITY_TYPES.IMAGE: image,
+            ENTITY_TYPES.LINK: link
             # Lambdas work too.
             ENTITY_TYPES.HORIZONTAL_RULE: lambda props: DOM.create_element('hr'),
             # Discard those entities.
             ENTITY_TYPES.EMBED: None,
             # Provide a fallback component (advanced).
-            ENTITY_TYPES.FALLBACK: EntityFallback,
+            ENTITY_TYPES.FALLBACK: entity_fallback,
         },
         'composite_decorators': [
             # Use composite decorators to replace text based on a regular expression.
-            BR,
-            Hashtag,
-            Linkify,
+            {
+                'strategy': re.compile(r'\n'),
+                'component': br,
+            },
+            {
+                'strategy': re.compile(r'#\w+'),
+                'component': hashtag,
+            },
+            {
+                'strategy': LINKIFY_RE,
+                'component': linkify,
+            },
         ],
     }
 
-See ``examples.py`` in the repository for more details.
+See `examples.py <https://github.com/springload/draftjs_exporter/blob/master/example.py>`_ for more details.
 
 Advanced usage
 --------------
@@ -149,7 +157,7 @@ Advanced usage
 Custom components
 ~~~~~~~~~~~~~~~~~
 
-To produce arbitrary markup with dynamic data, draftjs_exporter comes with an API to create rendering components. This API mirrors React's `createElement <https://facebook.github.io/react/docs/top-level-api.html#react.createelement>`_ API (what JSX compiles to).
+To generate arbitrary markup with dynamic data, draftjs_exporter comes with an API to create rendering components. This API mirrors React's `createElement <https://facebook.github.io/react/docs/top-level-api.html#react.createelement>`_ API (what JSX compiles to).
 
 .. code:: python
 
@@ -158,7 +166,7 @@ To produce arbitrary markup with dynamic data, draftjs_exporter comes with an AP
 
 
     # Components are simple functions that take `props` as parameter and return DOM elements.
-    def Image(props):
+    def image(props):
         # This component creates an image element, with the relevant attributes.
         return DOM.create_element('img', {
             'src': props.get('src'),
@@ -168,7 +176,7 @@ To produce arbitrary markup with dynamic data, draftjs_exporter comes with an AP
         })
 
 
-    def Blockquote(props):
+    def blockquote(props):
         # This component uses block data to render a blockquote.
         block_data = props['block']['data']
 
@@ -178,20 +186,20 @@ To produce arbitrary markup with dynamic data, draftjs_exporter comes with an AP
         }, props['children'])
 
 
-    class Button:
-        def render(self, props):
-            href = props.get('href', '#')
-            icon = props.get('icon', None)
-            text = props.get('text', '')
+    def button(props):
+        href = props.get('href', '#')
+        icon_name = props.get('icon', None)
+        text = props.get('text', '')
 
+        return DOM.create_element('a', {
+                'class': 'icon-text' if icon_name else None,
+                'href': href,
+            },
             # There can be as many `children` as required.
             # It is also possible to reuse other components and render them instead of HTML tags.
-            return DOM.create_element(
-                'a',
-                {'class': 'icon-text' if icon else None, 'href': href},
-                DOM.create_element(Icon, {'name': icon}) if icon else None,
-                DOM.create_element('span', {'class': 'icon-text__text'}, text) if icon else text
-            )
+            DOM.create_element(icon, {'name': icon_name}) if icon_name else None,
+            DOM.create_element('span', {'class': 'icon-text'}, text) if icon_name else text
+        )
 
 Apart from ``create_element``, a ``parse_html`` method is also available. Use it to interface with other HTML generators, like template engines.
 
@@ -211,7 +219,7 @@ Add the following to the exporter config,
     config = {
         'block_map': dict(BLOCK_MAP, **{
             # Provide a fallback for block types.
-            BLOCK_TYPES.FALLBACK: BlockFallback
+            BLOCK_TYPES.FALLBACK: block_fallback
         }),
     }
 
@@ -219,7 +227,7 @@ This fallback component can now control the exporter behavior when normal compon
 
 .. code:: python
 
-    def BlockFallback(props):
+    def block_fallback(props):
         type_ = props['block']['type']
 
         if type_ == 'example-discard':
diff --git a/benchmark.py b/benchmark.py
index a9714de..40ae663 100644
--- a/benchmark.py
+++ b/benchmark.py
@@ -5,6 +5,7 @@ from __future__ import absolute_import, unicode_literals
 import cProfile
 import logging
 import os
+import re
 from pstats import Stats
 from memory_profiler import profile
 
@@ -15,23 +16,23 @@ from draftjs_exporter.html import HTML
 
 from markov_draftjs import get_content_sample
 
-from example import ListItem, OrderedList, Image, BR, EntityFallback
+from example import list_item, ordered_list, image, br, entity_fallback
 
 
-def Document(props):
+def document(props):
     return DOM.create_element('a', {
         'title': props.get('label'),
         'href': '/documents/%s' % props.get('id'),
     }, props['children'])
 
 
-def Link(props):
+def link(props):
     return DOM.create_element('a', {
         'href': props['url'],
     }, props['children'])
 
 
-def BlockFallback(props):
+def block_fallback(props):
     type_ = props['block']['type']
 
     logging.warn('Missing config for "%s".' % type_)
@@ -48,22 +49,25 @@ config = {
             'wrapper_props': {'class': 'bullet-list'},
         },
         BLOCK_TYPES.ORDERED_LIST_ITEM: {
-            'element': ListItem,
-            'wrapper': OrderedList,
+            'element': list_item,
+            'wrapper': ordered_list,
         },
-        BLOCK_TYPES.FALLBACK: BlockFallback
+        BLOCK_TYPES.FALLBACK: block_fallback
     }),
     'style_map': STYLE_MAP,
     'entity_decorators': {
-        ENTITY_TYPES.IMAGE: Image,
-        ENTITY_TYPES.LINK: Link,
-        ENTITY_TYPES.DOCUMENT: Document,
+        ENTITY_TYPES.IMAGE: image,
+        ENTITY_TYPES.LINK: link,
+        ENTITY_TYPES.DOCUMENT: document,
         ENTITY_TYPES.HORIZONTAL_RULE: lambda props: DOM.create_element('hr'),
         ENTITY_TYPES.EMBED: None,
-        ENTITY_TYPES.FALLBACK: EntityFallback,
+        ENTITY_TYPES.FALLBACK: entity_fallback,
     },
     'composite_decorators': [
-        BR,
+        {
+            'strategy': re.compile(r'\n'),
+            'component': br,
+        }
     ],
     'engine': 'string',
 }
diff --git a/docs/example.md b/docs/example.md
index 299010b..c0fedf4 100644
--- a/docs/example.md
+++ b/docs/example.md
@@ -4,7 +4,7 @@
 -----
 <h2>
    draftjs_exporter is an HTML exporter for
-   <a href="https://github.com/facebook/draft-js" rel="noreferrer noopener" target="_blank">
+   <a href="https://github.com/facebook/draft-js">
     Draft.js
    </a>
    content
@@ -17,7 +17,7 @@
   </h3>
   <p>
    The exporter aims to provide sensible defaults from basic block types and inline styles to HTML, that can easily be customised when required. For more advanced scenarios, an API is provided (mimicking React's
-   <a href="https://facebook.github.io/react/docs/top-level-api.html#react.createelement" rel="noreferrer noopener" target="_blank">
+   <a href="https://facebook.github.io/react/docs/top-level-api.html#react.createelement">
     <code>
      createElement
     </code>
@@ -39,7 +39,7 @@
    </li>
    <li>
     Automatic conversion of entity data to HTML attributes (int &amp; boolean to string,
-    <a href="https://facebook.github.io/react/docs/jsx-in-depth.html" rel="noreferrer noopener" target="_blank">
+    <a href="https://facebook.github.io/react/docs/jsx-in-depth.html">
      <code>
       style object
      </code>
@@ -120,7 +120,7 @@
            #hashtag
           </span>
           support via
-          <a href="https://github.com/springload/draftjs_exporter/pull/17" rel="noreferrer noopener" target="_blank">
+          <a href="https://github.com/springload/draftjs_exporter/pull/17">
            #CompositeDecorators
           </a>
           .
@@ -177,7 +177,7 @@
     </ol>
    </li>
   </ol>
-  <pre><code>def Blockquote(props):
+  <pre><code>def blockquote(props):
     block_data = props['block']['data']
     return DOM.create_element('blockquote', {
         'cite': block_data.get('cite')
diff --git a/draftjs_exporter/composite_decorators.py b/draftjs_exporter/composite_decorators.py
index e8c8053..8080fd5 100644
--- a/draftjs_exporter/composite_decorators.py
+++ b/draftjs_exporter/composite_decorators.py
@@ -9,7 +9,7 @@ def get_decorations(decorators, text):
     decorations = []
 
     for decorator in decorators:
-        for match in decorator.SEARCH_RE.finditer(text):
+        for match in decorator['strategy'].finditer(text):
             begin, end = match.span()
             if not any(occupied.get(i) for i in range(begin, end)):
                 for i in range(begin, end):
@@ -29,7 +29,7 @@ def apply_decorators(decorators, text, block):
         if pointer < begin:
             yield text[pointer:begin]
 
-        yield DOM.create_element(decorator, {
+        yield DOM.create_element(decorator['component'], {
             'match': match,
             'block': {
                 'type': block['type'],
diff --git a/draftjs_exporter/defaults.py b/draftjs_exporter/defaults.py
index 89514f8..045ba56 100644
--- a/draftjs_exporter/defaults.py
+++ b/draftjs_exporter/defaults.py
@@ -12,7 +12,7 @@ def render_children(props):
     return props['children']
 
 
-def CodeBlock(props):
+def code_block(props):
     return DOM.create_element('pre', {}, DOM.create_element('code', {}, props['children']))
 
 
@@ -29,7 +29,7 @@ BLOCK_MAP = {
     BLOCK_TYPES.ORDERED_LIST_ITEM: {'element': 'li', 'wrapper': 'ol'},
     BLOCK_TYPES.BLOCKQUOTE: 'blockquote',
     BLOCK_TYPES.PRE: 'pre',
-    BLOCK_TYPES.CODE: CodeBlock,
+    BLOCK_TYPES.CODE: code_block,
     BLOCK_TYPES.ATOMIC: render_children,
 }
 
diff --git a/draftjs_exporter/dom.py b/draftjs_exporter/dom.py
index 406667b..e3b9424 100644
--- a/draftjs_exporter/dom.py
+++ b/draftjs_exporter/dom.py
@@ -80,13 +80,7 @@ class DOM(object):
         # The children prop is the first child if there is only one.
         props['children'] = children[0] if len(children) == 1 else children
 
-        if inspect.isclass(type_):
-            # Class component, not instantiated.
-            elt = type_().render(props)
-        elif callable(getattr(type_, 'render', None)):
-            # Class component, already instantiated.
-            elt = type_.render(props)
-        elif callable(type_):
+        if callable(type_):
             # Function component, via def or lambda.
             elt = type_(props)
         else:
diff --git a/example.py b/example.py
index 03684a1..bd4f657 100644
--- a/example.py
+++ b/example.py
@@ -17,7 +17,7 @@ from draftjs_exporter.dom import DOM
 from draftjs_exporter.html import HTML
 
 
-def Blockquote(props):
+def blockquote(props):
     block_data = props['block']['data']
 
     return DOM.create_element('blockquote', {
@@ -25,7 +25,7 @@ def Blockquote(props):
     }, props['children'])
 
 
-def ListItem(props):
+def list_item(props):
     depth = props['block']['depth']
 
     return DOM.create_element('li', {
@@ -33,7 +33,7 @@ def ListItem(props):
     }, props['children'])
 
 
-def OrderedList(props):
+def ordered_list(props):
     depth = props['block']['depth']
 
     return DOM.create_element('ol', {
@@ -41,7 +41,7 @@ def OrderedList(props):
     }, props['children'])
 
 
-def Image(props):
+def image(props):
     return DOM.create_element('img', {
         'src': props.get('src'),
         'width': props.get('width'),
@@ -50,78 +50,61 @@ def Image(props):
     })
 
 
-class Link:
-    def __init__(self, use_new_window=False):
-        self.use_new_window = use_new_window
-
-    def render(self, props):
-        link_props = {
-            'href': props['url'],
-        }
-
-        if self.use_new_window:
-            link_props['target'] = '_blank'
-            link_props['rel'] = 'noreferrer noopener'
-
-        return DOM.create_element('a', link_props, props['children'])
+def link(props):
+    return DOM.create_element('a', {
+        'href': props['url']
+    }, props['children'])
 
 
-class BR:
+def br(props):
     """
     Replace line breaks (\n) with br tags.
     """
-    SEARCH_RE = re.compile(r'\n')
-
-    def render(self, props):
-        # Do not process matches inside code blocks.
-        if props['block']['type'] == BLOCK_TYPES.CODE:
-            return props['children']
+    # Do not process matches inside code blocks.
+    if props['block']['type'] == BLOCK_TYPES.CODE:
+        return props['children']
 
-        return DOM.create_element('br')
+    return DOM.create_element('br')
 
 
-class Hashtag:
+def hashtag(props):
     """
     Wrap hashtags in spans with a specific class.
     """
-    SEARCH_RE = re.compile(r'#\w+')
+    # Do not process matches inside code blocks.
+    if props['block']['type'] == BLOCK_TYPES.CODE:
+        return props['children']
 
-    def render(self, props):
-        # Do not process matches inside code blocks.
-        if props['block']['type'] == BLOCK_TYPES.CODE:
-            return props['children']
+    return DOM.create_element('span', {'class': 'hashtag'}, props['children'])
 
-        return DOM.create_element('span', {'class': 'hashtag'}, props['children'])
 
+# See http://pythex.org/?regex=(http%3A%2F%2F%7Chttps%3A%2F%2F%7Cwww%5C.)(%5Ba-zA-Z0-9%5C.%5C-%25%2F%5C%3F%26_%3D%5C%2B%23%3A~!%2C%5C%27%5C*%5C%5E%24%5D%2B)&test_string=search%20http%3A%2F%2Fa.us%20or%20https%3A%2F%2Fyahoo.com%20or%20www.google.com%20for%20%23github%20and%20%23facebook&ignorecase=0&multiline=0&dotall=0&verbose=0
+LINKIFY_RE = re.compile(r'(http://|https://|www\.)([a-zA-Z0-9\.\-%/\?&_=\+#:~!,\'\*\^$]+)')
 
-class Linkify:
+
+def linkify(props):
     """
     Wrap plain URLs with link tags.
-    See http://pythex.org/?regex=(http%3A%2F%2F%7Chttps%3A%2F%2F%7Cwww%5C.)(%5Ba-zA-Z0-9%5C.%5C-%25%2F%5C%3F%26_%3D%5C%2B%23%3A~!%2C%5C%27%5C*%5C%5E%24%5D%2B)&test_string=search%20http%3A%2F%2Fa.us%20or%20https%3A%2F%2Fyahoo.com%20or%20www.google.com%20for%20%23github%20and%20%23facebook&ignorecase=0&multiline=0&dotall=0&verbose=0
-    for an example.
     """
-    SEARCH_RE = re.compile(r'(http://|https://|www\.)([a-zA-Z0-9\.\-%/\?&_=\+#:~!,\'\*\^$]+)')
-
-    def render(self, props):
-        match = props['match']
-        protocol = match.group(1)
-        url = match.group(2)
-        href = protocol + url
+    match = props['match']
+    protocol = match.group(1)
+    url = match.group(2)
+    href = protocol + url
 
-        if props['block']['type'] == BLOCK_TYPES.CODE:
-            return href
+    if props['block']['type'] == BLOCK_TYPES.CODE:
+        return href
 
-        link_props = {
-            'href': href,
-        }
+    link_props = {
+        'href': href,
+    }
 
-        if href.startswith('www'):
-            link_props['href'] = 'http://' + href
+    if href.startswith('www'):
+        link_props['href'] = 'http://' + href
 
-        return DOM.create_element('a', link_props, href)
+    return DOM.create_element('a', link_props, href)
 
 
-def BlockFallback(props):
+def block_fallback(props):
     type_ = props['block']['type']
 
     if type_ == 'example-discard':
@@ -138,7 +121,7 @@ def BlockFallback(props):
         return DOM.create_element('div', {}, props['children'])
 
 
-def EntityFallback(props):
+def entity_fallback(props):
     type_ = props['entity']['type']
     logging.warn('Missing config for "%s".' % type_)
     return DOM.create_element('span', {'class': 'missing-entity'}, props['children'])
@@ -160,13 +143,13 @@ if __name__ == '__main__':
                 'wrapper_props': {'class': 'bullet-list'},
             },
             # Use a custom component for more flexibility (reading block data or depth).
-            BLOCK_TYPES.BLOCKQUOTE: Blockquote,
+            BLOCK_TYPES.BLOCKQUOTE: blockquote,
             BLOCK_TYPES.ORDERED_LIST_ITEM: {
-                'element': ListItem,
-                'wrapper': OrderedList,
+                'element': list_item,
+                'wrapper': ordered_list,
             },
             # Provide a fallback component (advanced).
-            BLOCK_TYPES.FALLBACK: BlockFallback
+            BLOCK_TYPES.FALLBACK: block_fallback
         }),
         # `style_map` defines the HTML representation of inline elements.
         # Extend STYLE_MAP to start with sane defaults, or make your own from scratch.
@@ -178,21 +161,29 @@ if __name__ == '__main__':
         }),
         'entity_decorators': {
             # Map entities to components so they can be rendered with their data.
-            ENTITY_TYPES.IMAGE: Image,
-            # Components can be defined as classes to receive extra parameters.
-            ENTITY_TYPES.LINK: Link(use_new_window=True),
+            ENTITY_TYPES.IMAGE: image,
+            ENTITY_TYPES.LINK: link,
             # Lambdas work too.
             ENTITY_TYPES.HORIZONTAL_RULE: lambda props: DOM.create_element('hr'),
             # Discard those entities.
             ENTITY_TYPES.EMBED: None,
             # Provide a fallback component (advanced).
-            ENTITY_TYPES.FALLBACK: EntityFallback,
+            ENTITY_TYPES.FALLBACK: entity_fallback,
         },
         'composite_decorators': [
             # Use composite decorators to replace text based on a regular expression.
-            BR,
-            Hashtag,
-            Linkify,
+            {
+                'strategy': re.compile(r'\n'),
+                'component': br,
+            },
+            {
+                'strategy': re.compile(r'#\w+'),
+                'component': hashtag,
+            },
+            {
+                'strategy': LINKIFY_RE,
+                'component': linkify,
+            },
         ],
         # Specify which DOM backing engine to use.
         'engine': 'string',
@@ -579,7 +570,7 @@ if __name__ == '__main__':
             "data": {}
         }, {
             "key": "ed7hu",
-            "text": "def Blockquote(props):\n    block_data = props['block']['data']\n    return DOM.create_element('blockquote', {\n        'cite': block_data.get('cite')\n    }, props['children'])\n",
+            "text": "def blockquote(props):\n    block_data = props['block']['data']\n    return DOM.create_element('blockquote', {\n        'cite': block_data.get('cite')\n    }, props['children'])\n",
             "type": "code-block",
             "depth": 0,
             "inlineStyleRanges": [],


Test patch:
diff --git a/tests/test_composite_decorators.py b/tests/test_composite_decorators.py
index 94a8f2f..721f4dd 100644
--- a/tests/test_composite_decorators.py
+++ b/tests/test_composite_decorators.py
@@ -6,127 +6,64 @@ import unittest
 from draftjs_exporter.composite_decorators import render_decorators
 from draftjs_exporter.constants import BLOCK_TYPES
 from draftjs_exporter.dom import DOM
+from example import LINKIFY_RE, br, hashtag, linkify
 
+BR_DECORATOR = {
+    'strategy': re.compile(r'\n'),
+    'component': br,
+}
 
-class Linkify:
-    """
-    Wrap plain URLs with link tags.
-    See http://pythex.org/?regex=(http%3A%2F%2F%7Chttps%3A%2F%2F%7Cwww%5C.)(%5Ba-zA-Z0-9%5C.%5C-%25%2F%5C%3F%26_%3D%5C%2B%23%3A~!%2C%5C%27%5C*%5C%5E%24%5D%2B)&test_string=search%20http%3A%2F%2Fa.us%20or%20https%3A%2F%2Fyahoo.com%20or%20www.google.com%20for%20%23github%20and%20%23facebook&ignorecase=0&multiline=0&dotall=0&verbose=0
-    for an example.
-    """
-    SEARCH_RE = re.compile(r'(http://|https://|www\.)([a-zA-Z0-9\.\-%/\?&_=\+#:~!,\'\*\^$]+)')
+HASHTAG_DECORATOR = {
+    'strategy': re.compile(r'#\w+'),
+    'component': hashtag,
+}
 
-    def __init__(self, use_new_window=False):
-        self.use_new_window = use_new_window
-
-    def render(self, props):
-        match = props['match']
-        protocol = match.group(1)
-        url = match.group(2)
-        href = protocol + url
-
-        if props['block']['type'] == BLOCK_TYPES.CODE:
-            return href
-
-        link_props = {
-            'href': href,
-        }
-
-        if self.use_new_window:
-            link_props['target'] = '_blank'
-            link_props['rel'] = 'noreferrer noopener'
-
-        if href.startswith('www'):
-            link_props['href'] = 'http://' + href
-
-        return DOM.create_element('a', link_props, href)
-
-
-class Hashtag:
-    """
-    Wrap hashtags in spans with a specific class.
-    """
-    SEARCH_RE = re.compile(r'#\w+')
-
-    def render(self, props):
-        # Do not process matches inside code blocks.
-        if props['block']['type'] == BLOCK_TYPES.CODE:
-            return props['children']
-
-        return DOM.create_element('span', {'class': 'hashtag'}, props['children'])
-
-
-class BR:
-    """
-    Replace line breaks (\n) with br tags.
-    """
-    SEARCH_RE = re.compile(r'\n')
-
-    def render(self, props):
-        # Do not process matches inside code blocks.
-        if props['block']['type'] == BLOCK_TYPES.CODE:
-            return props['children']
-
-        return DOM.create_element('br')
+LINKIFY_DECORATOR = {
+    'strategy': LINKIFY_RE,
+    'component': linkify,
+}
 
 
 class TestLinkify(unittest.TestCase):
-    def test_init(self):
-        self.assertIsInstance(Linkify(), Linkify)
-
     def test_render(self):
-        match = next(Linkify.SEARCH_RE.finditer('test https://www.example.com'))
+        match = next(LINKIFY_DECORATOR['strategy'].finditer('test https://www.example.com'))
 
-        self.assertEqual(DOM.render(DOM.create_element(Linkify, {
+        self.assertEqual(DOM.render(DOM.create_element(LINKIFY_DECORATOR['component'], {
             'block': {'type': BLOCK_TYPES.UNSTYLED},
             'match': match,
         }, match.group(0))), '<a href="https://www.example.com">https://www.example.com</a>')
 
     def test_render_www(self):
-        match = next(Linkify.SEARCH_RE.finditer('test www.example.com'))
+        match = next(LINKIFY_DECORATOR['strategy'].finditer('test www.example.com'))
 
-        self.assertEqual(DOM.render(DOM.create_element(Linkify, {
+        self.assertEqual(DOM.render(DOM.create_element(LINKIFY_DECORATOR['component'], {
             'block': {'type': BLOCK_TYPES.UNSTYLED},
             'match': match,
         }, match.group(0))), '<a href="http://www.example.com">www.example.com</a>')
 
     def test_render_code_block(self):
-        match = next(Linkify.SEARCH_RE.finditer('test https://www.example.com'))
+        match = next(LINKIFY_DECORATOR['strategy'].finditer('test https://www.example.com'))
 
-        self.assertEqual(DOM.create_element(Linkify, {
+        self.assertEqual(DOM.create_element(LINKIFY_DECORATOR['component'], {
             'block': {'type': BLOCK_TYPES.CODE},
             'match': match,
         }, match.group(0)), match.group(0))
 
-    def test_render_new_window(self):
-        match = next(Linkify.SEARCH_RE.finditer('test https://www.example.com'))
-
-        self.assertEqual(DOM.render(DOM.create_element(Linkify(use_new_window=True), {
-            'block': {'type': BLOCK_TYPES.UNSTYLED},
-            'match': match,
-        }, match.group(0))), '<a href="https://www.example.com" rel="noreferrer noopener" target="_blank">https://www.example.com</a>')
-
 
 class TestHashtag(unittest.TestCase):
-    def test_init(self):
-        self.assertIsInstance(Hashtag(), Hashtag)
-
     def test_render(self):
-        self.assertEqual(DOM.render(DOM.create_element(Hashtag, {'block': {'type': BLOCK_TYPES.UNSTYLED}}, '#hashtagtest')), '<span class="hashtag">#hashtagtest</span>')
+        self.assertEqual(DOM.render(DOM.create_element(HASHTAG_DECORATOR['component'], {'block': {'type': BLOCK_TYPES.UNSTYLED}}, '#hashtagtest')), '<span class="hashtag">#hashtagtest</span>')
 
     def test_render_code_block(self):
-        self.assertEqual(DOM.render(DOM.create_element(Hashtag, {'block': {'type': BLOCK_TYPES.CODE}}, '#hashtagtest')), '#hashtagtest')
+        self.assertEqual(DOM.render(DOM.create_element(HASHTAG_DECORATOR['component'], {'block': {'type': BLOCK_TYPES.CODE}}, '#hashtagtest')), '#hashtagtest')
 
 
 class TestBR(unittest.TestCase):
-    def test_init(self):
-        self.assertIsInstance(BR(), BR)
-
     def test_render(self):
-        self.assertEqual(DOM.render(DOM.create_element(BR, {'block': {'type': BLOCK_TYPES.UNSTYLED}}, '\n')), '<br/>')
+        self.assertEqual(DOM.render(DOM.create_element(BR_DECORATOR['component'], {'block': {'type': BLOCK_TYPES.UNSTYLED}}, '\n')), '<br/>')
 
     def test_render_code_block(self):
-        self.assertEqual(DOM.create_element(BR, {'block': {'type': BLOCK_TYPES.CODE}}, '\n'), '\n')
+        self.assertEqual(DOM.create_element(BR_DECORATOR['component'], {'block': {'type': BLOCK_TYPES.CODE}}, '\n'), '\n')
 
 
 class TestCompositeDecorators(unittest.TestCase):
@@ -134,10 +71,10 @@ class TestCompositeDecorators(unittest.TestCase):
         self.assertEqual(DOM.render(render_decorators([], 'test https://www.example.com#hash #hashtagtest', {'type': BLOCK_TYPES.UNSTYLED, 'depth': 0})), 'test https://www.example.com#hash #hashtagtest')
 
     def test_render_decorators_single(self):
-        self.assertEqual(DOM.render(render_decorators([Linkify()], 'test https://www.example.com#hash #hashtagtest', {'type': BLOCK_TYPES.UNSTYLED, 'depth': 0})), 'test <a href="https://www.example.com#hash">https://www.example.com#hash</a> #hashtagtest')
+        self.assertEqual(DOM.render(render_decorators([LINKIFY_DECORATOR], 'test https://www.example.com#hash #hashtagtest', {'type': BLOCK_TYPES.UNSTYLED, 'depth': 0})), 'test <a href="https://www.example.com#hash">https://www.example.com#hash</a> #hashtagtest')
 
     def test_render_decorators_conflicting_order_one(self):
-        self.assertEqual(DOM.render(render_decorators([Linkify(), Hashtag()], 'test https://www.example.com#hash #hashtagtest', {'type': BLOCK_TYPES.UNSTYLED, 'depth': 0})), 'test <a href="https://www.example.com#hash">https://www.example.com#hash</a> <span class="hashtag">#hashtagtest</span>')
+        self.assertEqual(DOM.render(render_decorators([LINKIFY_DECORATOR, HASHTAG_DECORATOR], 'test https://www.example.com#hash #hashtagtest', {'type': BLOCK_TYPES.UNSTYLED, 'depth': 0})), 'test <a href="https://www.example.com#hash">https://www.example.com#hash</a> <span class="hashtag">#hashtagtest</span>')
 
     def test_render_decorators_conflicting_order_two(self):
-        self.assertEqual(DOM.render(render_decorators([Hashtag(), Linkify()], 'test https://www.example.com#hash #hashtagtest', {'type': BLOCK_TYPES.UNSTYLED, 'depth': 0})), 'test https://www.example.com<span class="hashtag">#hash</span> <span class="hashtag">#hashtagtest</span>')
+        self.assertEqual(DOM.render(render_decorators([HASHTAG_DECORATOR, LINKIFY_DECORATOR], 'test https://www.example.com#hash #hashtagtest', {'type': BLOCK_TYPES.UNSTYLED, 'depth': 0})), 'test https://www.example.com<span class="hashtag">#hash</span> <span class="hashtag">#hashtagtest</span>')
diff --git a/tests/test_defaults.py b/tests/test_defaults.py
index 14bdcd7..58839d7 100644
--- a/tests/test_defaults.py
+++ b/tests/test_defaults.py
@@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals
 
 import unittest
 
-from draftjs_exporter.defaults import BLOCK_MAP, STYLE_MAP, CodeBlock, render_children
+from draftjs_exporter.defaults import BLOCK_MAP, STYLE_MAP, code_block, render_children
 from draftjs_exporter.dom import DOM
 
 
@@ -16,7 +16,5 @@ class TestDefaults(unittest.TestCase):
     def test_render_children(self):
         self.assertEqual(render_children({'children': 'test'}), 'test')
 
-
-class TestCodeBlock(unittest.TestCase):
-    def test_render(self):
-        self.assertEqual(DOM.render_debug(CodeBlock({'children': 'test'})), '<pre><code>test</code></pre>')
+    def test_render_code_block(self):
+        self.assertEqual(DOM.render_debug(code_block({'children': 'test'})), '<pre><code>test</code></pre>')
diff --git a/tests/test_dom.py b/tests/test_dom.py
index 0695b50..e4b7617 100644
--- a/tests/test_dom.py
+++ b/tests/test_dom.py
@@ -6,7 +6,7 @@ from draftjs_exporter.dom import DOM
 from draftjs_exporter.engines.html5lib import DOM_HTML5LIB
 from draftjs_exporter.engines.lxml import DOM_LXML
 from draftjs_exporter.error import ConfigException
-from tests.test_entities import Icon
+from tests.test_entities import icon
 
 
 class DOMTestImpl(object):
@@ -54,10 +54,7 @@ class TestDOM(unittest.TestCase):
         self.assertEqual(DOM.render_debug(DOM.create_element('a', {}, None, DOM.create_element('span', {}, 'Test test'))), '<a><span>Test test</span></a>')
 
     def test_create_element_entity(self):
-        self.assertEqual(DOM.render_debug(DOM.create_element(Icon, {'name': 'rocket'})), '<svg class="icon"><use xlink:href="#icon-rocket"></use></svg>')
-
-    def test_create_element_entity_configured(self):
-        self.assertEqual(DOM.render_debug(DOM.create_element(Icon(icon_class='i'), {'name': 'rocket'})), '<svg class="i"><use xlink:href="#icon-rocket"></use></svg>')
+        self.assertEqual(DOM.render_debug(DOM.create_element(icon, {'name': 'rocket'})), '<svg class="icon"><use xlink:href="#icon-rocket"></use></svg>')
 
     def test_parse_html(self):
         self.assertEqual(DOM.render_debug(DOM.parse_html('<p><span>Test text</span></p>')), '<p><span>Test text</span></p>')
diff --git a/tests/test_entities.py b/tests/test_entities.py
index d80a3b5..eea6c4e 100644
--- a/tests/test_entities.py
+++ b/tests/test_entities.py
@@ -5,25 +5,20 @@ import unittest
 from draftjs_exporter.dom import DOM
 
 
-def Null(props):
-    return DOM.create_element()
-
-
-def HR(props):
+def hr(props):
     return DOM.create_element('hr')
 
 
-class Link:
-    def render(self, props):
-        attributes = {}
-        for key in props:
-            attr = key if key != 'url' else 'href'
-            attributes[attr] = props[key]
+def link(props):
+    attributes = {}
+    for key in props:
+        attr = key if key != 'url' else 'href'
+        attributes[attr] = props[key]
 
-        return DOM.create_element('a', attributes, props['children'])
+    return DOM.create_element('a', attributes, props['children'])
 
 
-def Image(props):
+def image(props):
     return DOM.create_element('img', {
         'src': props.get('src'),
         'width': props.get('width'),
@@ -32,45 +27,32 @@ def Image(props):
     })
 
 
-class Icon:
-    def __init__(self, icon_class='icon'):
-        self.icon_class = icon_class
+def icon(props):
+    href = '#icon-%s' % props.get('name', '')
+    return DOM.create_element('svg', {'class': 'icon'}, DOM.create_element('use', {'xlink:href': href}))
 
-    def render(self, props):
-        href = '#icon-%s' % props.get('name', '')
-        return DOM.create_element('svg', {'class': self.icon_class}, DOM.create_element('use', {'xlink:href': href}))
 
+def button(props):
+    href = props.get('href', '#')
+    icon_name = props.get('icon', None)
+    text = props.get('text', '')
 
-class Button:
-    def render(self, props):
-        href = props.get('href', '#')
-        icon = props.get('icon', None)
-        text = props.get('text', '')
-
-        return DOM.create_element(
-            'a',
-            {'class': 'icon-text' if icon else None, 'href': href},
-            DOM.create_element(Icon, {'name': icon}) if icon else None,
-            DOM.create_element('span', {'class': 'icon-text__text'}, text) if icon else text
-        )
-
-
-class TestNull(unittest.TestCase):
-    def test_render(self):
-        self.assertEqual(DOM.render(DOM.create_element(Null)), '')
+    return DOM.create_element(
+        'a',
+        {'class': 'icon-text' if icon_name else None, 'href': href},
+        DOM.create_element(icon, {'name': icon_name}) if icon_name else None,
+        DOM.create_element('span', {'class': 'icon-text__text'}, text) if icon_name else text
+    )
 
 
 class TestIcon(unittest.TestCase):
     def test_render(self):
-        self.assertEqual(DOM.render(DOM.create_element(Icon, {'name': 'rocket'})), '<svg class="icon"><use xlink:href="#icon-rocket"></use></svg>')
-
-    def test_render_configured(self):
-        self.assertEqual(DOM.render(DOM.create_element(Icon(icon_class='i'), {'name': 'rocket'})), '<svg class="i"><use xlink:href="#icon-rocket"></use></svg>')
+        self.assertEqual(DOM.render(DOM.create_element(icon, {'name': 'rocket'})), '<svg class="icon"><use xlink:href="#icon-rocket"></use></svg>')
 
 
 class TestImage(unittest.TestCase):
     def test_render(self):
-        self.assertEqual(DOM.render(DOM.create_element(Image, {
+        self.assertEqual(DOM.render(DOM.create_element(image, {
             'src': 'http://example.com/example.png',
             'width': 320,
             'height': 580,
@@ -79,21 +61,21 @@ class TestImage(unittest.TestCase):
 
 class TestLink(unittest.TestCase):
     def test_render(self):
-        self.assertEqual(DOM.render(DOM.create_element(Link, {
+        self.assertEqual(DOM.render(DOM.create_element(link, {
             'url': 'http://example.com',
         }, 'wow')), '<a href="http://example.com">wow</a>')
 
 
 class TestButton(unittest.TestCase):
     def test_render_with_icon(self):
-        self.assertEqual(DOM.render(DOM.create_element(Button, {
+        self.assertEqual(DOM.render(DOM.create_element(button, {
             'href': 'http://example.com',
             'icon': 'rocket',
             'text': 'Launch',
         })), '<a class="icon-text" href="http://example.com"><svg class="icon"><use xlink:href="#icon-rocket"></use></svg><span class="icon-text__text">Launch</span></a>')
 
     def test_render_without_icon(self):
-        self.assertEqual(DOM.render(DOM.create_element(Button, {
+        self.assertEqual(DOM.render(DOM.create_element(button, {
             'href': 'http://example.com',
             'text': 'Launch',
         })), '<a href="http://example.com">Launch</a>')
diff --git a/tests/test_entity_state.py b/tests/test_entity_state.py
index 7c91ff0..743d76d 100644
--- a/tests/test_entity_state.py
+++ b/tests/test_entity_state.py
@@ -4,10 +4,10 @@ import unittest
 
 from draftjs_exporter.command import Command
 from draftjs_exporter.entity_state import EntityException, EntityState
-from tests.test_entities import Link
+from tests.test_entities import link
 
 entity_decorators = {
-    'LINK': Link()
+    'LINK': link
 }
 
 entity_map = {
diff --git a/tests/test_exports.py b/tests/test_exports.py
index f9b8650..dfd50a6 100644
--- a/tests/test_exports.py
+++ b/tests/test_exports.py
@@ -12,23 +12,23 @@ from draftjs_exporter.constants import BLOCK_TYPES, ENTITY_TYPES
 from draftjs_exporter.defaults import BLOCK_MAP, STYLE_MAP
 from draftjs_exporter.dom import DOM
 from draftjs_exporter.html import HTML
-from tests.test_composite_decorators import BR, Hashtag, Linkify
-from tests.test_entities import HR, Image, Link
+from tests.test_composite_decorators import BR_DECORATOR, HASHTAG_DECORATOR, LINKIFY_DECORATOR
+from tests.test_entities import hr, image, link
 
 fixtures_path = os.path.join(os.path.dirname(__file__), 'test_exports.json')
 fixtures = json.loads(open(fixtures_path, 'r').read())
 
 exporter = HTML({
     'entity_decorators': {
-        ENTITY_TYPES.LINK: Link,
-        ENTITY_TYPES.IMAGE: Image,
-        ENTITY_TYPES.HORIZONTAL_RULE: HR,
+        ENTITY_TYPES.LINK: link,
+        ENTITY_TYPES.HORIZONTAL_RULE: hr,
+        ENTITY_TYPES.IMAGE: image,
         ENTITY_TYPES.EMBED: None,
     },
     'composite_decorators': [
-        BR,
-        Linkify,
-        Hashtag,
+        BR_DECORATOR,
+        LINKIFY_DECORATOR,
+        HASHTAG_DECORATOR,
     ],
     'block_map': dict(BLOCK_MAP, **{
         BLOCK_TYPES.UNORDERED_LIST_ITEM: {
diff --git a/tests/test_output.py b/tests/test_output.py
index cc643f1..fac2bfb 100644
--- a/tests/test_output.py
+++ b/tests/test_output.py
@@ -7,20 +7,20 @@ from draftjs_exporter.constants import BLOCK_TYPES, ENTITY_TYPES, INLINE_STYLES
 from draftjs_exporter.defaults import BLOCK_MAP
 from draftjs_exporter.entity_state import EntityException
 from draftjs_exporter.html import HTML
-from tests.test_composite_decorators import BR, Hashtag, Linkify
-from tests.test_entities import HR, Image, Link
-from tests.test_wrapper_state import Blockquote
+from tests.test_composite_decorators import BR_DECORATOR, HASHTAG_DECORATOR, LINKIFY_DECORATOR
+from tests.test_entities import hr, image, link
+from tests.test_wrapper_state import blockquote
 
 config = {
     'entity_decorators': {
-        ENTITY_TYPES.LINK: Link,
-        ENTITY_TYPES.HORIZONTAL_RULE: HR,
-        ENTITY_TYPES.IMAGE: Image
+        ENTITY_TYPES.LINK: link,
+        ENTITY_TYPES.HORIZONTAL_RULE: hr,
+        ENTITY_TYPES.IMAGE: image
     },
     'composite_decorators': [
-        Linkify,
-        Hashtag,
-        BR,
+        LINKIFY_DECORATOR,
+        HASHTAG_DECORATOR,
+        BR_DECORATOR,
     ],
     'block_map': dict(BLOCK_MAP, **{
         BLOCK_TYPES.UNORDERED_LIST_ITEM: {
@@ -29,7 +29,7 @@ config = {
             'wrapper_props': {'class': 'steps'},
         },
         'blockquote': {
-            'element': Blockquote,
+            'element': blockquote,
             'wrapper': 'div',
         },
     }),
@@ -924,7 +924,7 @@ class TestOutput(unittest.TestCase):
     def test_render_with_big_content(self):
         self.assertEqual(HTML({
             'entity_decorators': {
-                'LINK': Link()
+                'LINK': link
             },
             'block_map': {
                 'header-two': {'element': 'h2'},
diff --git a/tests/test_wrapper_state.py b/tests/test_wrapper_state.py
index ca58cd5..60deb78 100644
--- a/tests/test_wrapper_state.py
+++ b/tests/test_wrapper_state.py
@@ -4,30 +4,7 @@ import unittest
 
 from draftjs_exporter.dom import DOM
 from draftjs_exporter.wrapper_state import WrapperState
-
-
-def Blockquote(props):
-    block_data = props['block']['data']
-
-    return DOM.create_element('blockquote', {
-        'cite': block_data.get('cite')
-    }, props['children'])
-
-
-def ListItem(props):
-    depth = props['block']['depth']
-
-    return DOM.create_element('li', {
-        'class': 'list-item--depth-{0}'.format(depth)
-    }, props['children'])
-
-
-def OrderedList(props):
-    depth = props['block']['depth']
-
-    return DOM.create_element('ol', {
-        'class': 'list--depth-{0}'.format(depth)
-    }, props['children'])
+from example import blockquote, list_item, ordered_list
 
 
 class TestWrapperState(unittest.TestCase):
@@ -39,10 +16,10 @@ class TestWrapperState(unittest.TestCase):
             'unstyled': 'div',
             'atomic': lambda props: props['children'],
             'ignore': None,
-            'blockquote': Blockquote,
+            'blockquote': blockquote,
             'ordered-list-item': {
-                'element': ListItem,
-                'wrapper': OrderedList
+                'element': list_item,
+                'wrapper': ordered_list
             },
         })
 
@@ -132,7 +109,7 @@ class TestBlockquote(unittest.TestCase):
         DOM.use(DOM.HTML5LIB)
 
     def test_render_debug(self):
-        self.assertEqual(DOM.render_debug(DOM.create_element(Blockquote, {
+        self.assertEqual(DOM.render_debug(DOM.create_element(blockquote, {
             'block': {
                 'data': {
                     'cite': 'http://example.com/',
@@ -146,7 +123,7 @@ class TestListItem(unittest.TestCase):
         DOM.use(DOM.HTML5LIB)
 
     def test_render_debug(self):
-        self.assertEqual(DOM.render_debug(DOM.create_element(ListItem, {
+        self.assertEqual(DOM.render_debug(DOM.create_element(list_item, {
             'block': {
                 'depth': 5,
             },