Example #1
0
def export():
    if request.json is None:
        abort(400)

    exporter_config = request.json["exporterConfig"]

    entity_decorators = {}
    block_map = dict(BLOCK_MAP, **exporter_config.get("block_map", {}))
    style_map = dict(STYLE_MAP, **exporter_config.get("style_map", {}))

    entity_decorators[ENTITY_TYPES.FALLBACK] = import_decorator(
        "missing_inline"
    )
    block_map[BLOCK_TYPES.FALLBACK] = import_decorator("missing_block")
    style_map[INLINE_STYLES.FALLBACK] = import_decorator("missing_inline")

    for type_, value in exporter_config.get("entity_decorators", {}).items():
        entity_decorators[type_] = import_decorator(value)

    exporter = HTML(
        {
            "entity_decorators": entity_decorators,
            "block_map": block_map,
            "style_map": style_map,
        }
    )

    html = exporter.render(request.json["contentState"])
    markdown = render_markdown(request.json["contentState"])

    return json.dumps(
        {"html": html, "markdown": markdown, "prettified": prettify(html)}
    )
def export():
    if request.json is None:
        abort(400)

    exporter_config = request.json['exporterConfig']

    entity_decorators = {}
    block_map = dict(BLOCK_MAP, **exporter_config.get('block_map', {}))
    style_map = dict(STYLE_MAP, **exporter_config.get('style_map', {}))

    entity_decorators[ENTITY_TYPES.FALLBACK] = import_decorator(
        'missing_inline')
    block_map[BLOCK_TYPES.FALLBACK] = import_decorator('missing_block')
    style_map[INLINE_STYLES.FALLBACK] = import_decorator('missing_inline')

    for type_, value in exporter_config.get('entity_decorators',
                                            {}).iteritems():
        entity_decorators[type_] = import_decorator(value)

    exporter = HTML({
        'entity_decorators': entity_decorators,
        'block_map': block_map,
        'style_map': style_map,
    })

    html = exporter.render(request.json['contentState'])

    return json.dumps({
        'html': html,
        'prettified': prettify(html),
    })
Example #3
0
class ContentstateConverter:
    def __init__(self, features=None):
        self.features = features
        self.html_to_contentstate_handler = HtmlToContentStateHandler(features)

        exporter_config = {
            "block_map": {
                "unstyled": persist_key_for_block("p"),
                "atomic": render_children,
                "fallback": block_fallback,
            },
            "style_map": {
                "FALLBACK": style_fallback,
            },
            "entity_decorators": {
                "FALLBACK": entity_fallback,
            },
            "composite_decorators": [
                {
                    "strategy": re.compile(r"\n"),
                    "component": br,
                },
            ],
            "engine": DOM.STRING,
        }

        for feature in self.features:
            rule = feature_registry.get_converter_rule("contentstate", feature)
            if rule is not None:
                feature_config = rule["to_database_format"]
                exporter_config["block_map"].update(
                    {
                        block_type: persist_key_for_block(config)
                        for block_type, config in feature_config.get(
                            "block_map", {}
                        ).items()
                    }
                )
                exporter_config["style_map"].update(feature_config.get("style_map", {}))
                exporter_config["entity_decorators"].update(
                    feature_config.get("entity_decorators", {})
                )

        self.exporter = HTMLExporter(exporter_config)

    def from_database_format(self, html):
        self.html_to_contentstate_handler.reset()
        self.html_to_contentstate_handler.feed(html)
        self.html_to_contentstate_handler.close()

        return self.html_to_contentstate_handler.contentstate.as_json(
            indent=4, separators=(",", ": ")
        )

    def to_database_format(self, contentstate_json):
        return self.exporter.render(json.loads(contentstate_json))
Example #4
0
class ContentstateConverter():
    def __init__(self, features=None):
        self.features = features
        self.html_to_contentstate_handler = HtmlToContentStateHandler(features)

        exporter_config = {
            'block_map': {
                'unstyled': persist_key_for_block('p'),
                'atomic': render_children,
                'fallback': block_fallback,
            },
            'style_map': {
                'FALLBACK': style_fallback,
            },
            'entity_decorators': {
                'FALLBACK': entity_fallback,
            },
            'composite_decorators': [
                {
                    'strategy': re.compile(r'\n'),
                    'component': br,
                },
            ],
            'engine':
            DOM.STRING,
        }

        for feature in self.features:
            rule = feature_registry.get_converter_rule('contentstate', feature)
            if rule is not None:
                feature_config = rule['to_database_format']
                exporter_config['block_map'].update({
                    block_type: persist_key_for_block(config)
                    for block_type, config in feature_config.get(
                        'block_map', {}).items()
                })
                exporter_config['style_map'].update(
                    feature_config.get('style_map', {}))
                exporter_config['entity_decorators'].update(
                    feature_config.get('entity_decorators', {}))

        self.exporter = HTMLExporter(exporter_config)

    def from_database_format(self, html):
        self.html_to_contentstate_handler.reset()
        self.html_to_contentstate_handler.feed(html)
        self.html_to_contentstate_handler.close()

        return self.html_to_contentstate_handler.contentstate.as_json(
            indent=4, separators=(',', ': '))

    def to_database_format(self, contentstate_json):
        return self.exporter.render(json.loads(contentstate_json))
Example #5
0
class ContentstateConverter():
    def __init__(self, features=None):
        self.features = features
        self.html_to_contentstate_handler = HtmlToContentStateHandler(features)

        exporter_config = {
            'block_map': {
                'unstyled': 'p',
                'atomic': render_children,
                'fallback': block_fallback,
            },
            'style_map': {},
            'entity_decorators': {
                'FALLBACK': entity_fallback,
            },
            'composite_decorators': [
                {
                    'strategy': re.compile(r'\n'),
                    'component': br,
                },
            ],
            'engine':
            DOM.STRING,
        }

        for feature in self.features:
            rule = feature_registry.get_converter_rule('contentstate', feature)
            if rule is not None:
                feature_config = rule['to_database_format']
                exporter_config['block_map'].update(
                    feature_config.get('block_map', {}))
                exporter_config['style_map'].update(
                    feature_config.get('style_map', {}))
                exporter_config['entity_decorators'].update(
                    feature_config.get('entity_decorators', {}))

        self.exporter = HTMLExporter(exporter_config)

    def from_database_format(self, html):
        self.html_to_contentstate_handler.reset()
        self.html_to_contentstate_handler.feed(html)

        if not self.html_to_contentstate_handler.contentstate.blocks:
            # Draftail does not accept an empty block list as valid, but does accept 'null' as meaning "no content"
            return 'null'

        return self.html_to_contentstate_handler.contentstate.as_json(
            indent=4, separators=(',', ': '))

    def to_database_format(self, contentstate_json):
        return self.exporter.render(json.loads(contentstate_json))
class DraftText(RichText):
    def __init__(self, value, **kwargs):
        super(DraftText, self).__init__(value or '{}', **kwargs)
        self.exporter = HTML(get_exporter_config())

    def get_json(self):
        return self.source

    @cached_property
    def _html(self):
        return self.exporter.render(json.loads(self.source))

    def __html__(self):
        return self._html

    def __eq__(self, other):
        return hasattr(other, '__html__') and self.__html__() == other.__html__()
Example #7
0
class ContentstateConverter():
    def __init__(self, features=None):
        self.features = features
        self.html_to_contentstate_handler = HtmlToContentStateHandler(features)

        exporter_config = {
            'block_map': {
                'unstyled': 'p',
                'atomic': render_children,
                'fallback': block_fallback,
            },
            'style_map': {},
            'entity_decorators': {
                'FALLBACK': entity_fallback,
            },
            'composite_decorators': [
                {
                    'strategy': re.compile(r'\n'),
                    'component': br,
                },
            ],
            'engine': DOM.STRING,
        }

        for feature in self.features:
            rule = feature_registry.get_converter_rule('contentstate', feature)
            if rule is not None:
                feature_config = rule['to_database_format']
                exporter_config['block_map'].update(feature_config.get('block_map', {}))
                exporter_config['style_map'].update(feature_config.get('style_map', {}))
                exporter_config['entity_decorators'].update(feature_config.get('entity_decorators', {}))

        self.exporter = HTMLExporter(exporter_config)

    def from_database_format(self, html):
        self.html_to_contentstate_handler.reset()
        self.html_to_contentstate_handler.feed(html)

        if not self.html_to_contentstate_handler.contentstate.blocks:
            # Draftail does not accept an empty block list as valid, but does accept 'null' as meaning "no content"
            return 'null'

        return self.html_to_contentstate_handler.contentstate.as_json(indent=4, separators=(',', ': '))

    def to_database_format(self, contentstate_json):
        return self.exporter.render(json.loads(contentstate_json))
Example #8
0
    def do_POST(self):
        try:
            content_length = int(self.headers["Content-Length"])
            post_data = self.rfile.read(content_length)
            request_json = json.loads(post_data)
        except Exception:
            self.send_response(400)
            return

        exporter_config = request_json["exporterConfig"]

        entity_decorators = {}
        block_map = dict(BLOCK_MAP, **exporter_config.get("block_map", {}))
        style_map = dict(STYLE_MAP, **exporter_config.get("style_map", {}))

        entity_decorators[ENTITY_TYPES.FALLBACK] = missing_inline
        block_map[BLOCK_TYPES.FALLBACK] = missing_block
        style_map[INLINE_STYLES.FALLBACK] = missing_inline

        for type_, value in exporter_config.get("entity_decorators",
                                                {}).items():
            entity_decorators[type_] = import_decorator(value)

        exporter = HTML({
            "entity_decorators": entity_decorators,
            "block_map": block_map,
            "style_map": style_map,
        })

        html = exporter.render(request_json["contentState"])
        markdown = render_markdown(request_json["contentState"])

        ret = json.dumps({
            "html": html,
            "markdown": markdown,
            "prettified": prettify(html),
            "version": __version__,
        })

        self.send_response(200)
        self.send_header("Content-type", "application/json; charset=utf-8")
        self.end_headers()
        self.wfile.write(ret.encode("utf8"))
        return
Example #9
0
class TestOutput(unittest.TestCase):
    """
    Test cases related to specific features of the HTML builder.
    """

    def setUp(self):
        self.maxDiff = None
        self.exporter = HTML(config)

    def test_render_empty(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': []
        }), '')

    def test_render_with_different_blocks(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '5s7g9',
                    'text': 'Header',
                    'type': 'header-one',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }
            ]
        }), '<h1>Header</h1><p>some paragraph text</p>')

    def test_render_with_unicode(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'Emojis! 🍺',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }
            ]
        }), '<p>Emojis! \U0001f37a</p>')

    def test_render_with_inline_styles(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 4,
                            'style': 'ITALIC'
                        }
                    ],
                    'entityRanges': []
                }
            ]
        }), '<p><em>some</em> paragraph text</p>')

    def test_render_with_multiple_inline_styles(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {
                '0': {
                    'type': 'LINK',
                    'mutability': 'MUTABLE',
                    'data': {
                        'url': 'http://example.com'
                    }
                }
            },
            'blocks': [
                {
                    'key': '5s7g9',
                    'text': 'Header',
                    'type': 'header-one',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 2,
                            'style': 'BOLD'
                        }
                    ],
                    'entityRanges': []
                },
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 4,
                            'style': 'HIGHLIGHT'
                        }
                    ],
                    'entityRanges': [
                        {
                            'offset': 5,
                            'length': 9,
                            'key': 0
                        }
                    ]
                }
            ]
        }), '<h1><strong>He</strong>ader</h1><p><strong style="text-decoration: underline;">some</strong> <a href="http://example.com">paragraph</a> text</p>')

    def test_render_with_entities(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {
                '0': {
                    'type': 'LINK',
                    'mutability': 'MUTABLE',
                    'data': {
                        'url': 'http://example.com'
                    }
                }
            },
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [
                        {
                            'offset': 5,
                            'length': 9,
                            'key': 0
                        }
                    ]
                }
            ]
        }), '<p>some <a href="http://example.com">paragraph</a> text</p>')

    def test_render_with_entities_crossing_raises(self):
        with self.assertRaises(EntityException):
            self.exporter.render({
                'entityMap': {
                    '0': {
                        'type': 'LINK',
                        'mutability': 'MUTABLE',
                        'data': {
                            'url': 'http://example.com'
                        }
                    },
                    '1': {
                        'type': 'LINK',
                        'mutability': 'MUTABLE',
                        'data': {
                            'url': 'http://bar.example.com'
                        }
                    }
                },
                'blocks': [
                    {
                        'key': 'dem5p',
                        'text': 'some paragraph text',
                        'type': 'unstyled',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': [
                            {
                                'offset': 5,
                                'length': 9,
                                'key': 0
                            },
                            {
                                'offset': 2,
                                'length': 9,
                                'key': 1
                            }
                        ]
                    }
                ]
            })

    def test_render_with_styles_in_entities(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {
                '0': {
                    'type': 'LINK',
                    'mutability': 'MUTABLE',
                    'data': {
                        'url': 'http://example.com'
                    }
                },
                '1': {
                    'type': 'HORIZONTAL_RULE',
                    'mutability': 'IMMUTABLE',
                    'data': {},
                },
            },
            'blocks': [
                {
                    'key': 'f4gp0',
                    'text': 'test style object to style string).',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 5,
                            'length': 12,
                            'style': 'CODE',
                        },
                        {
                            'offset': 21,
                            'length': 12,
                            'style': 'CODE',
                        }
                    ],
                    'entityRanges': [
                        {
                            'offset': 5,
                            'length': 28,
                            'key': 0
                        },
                    ],
                    'data': {},
                }, {
                    'key': 'f4fp0',
                    'text': ' ',
                    'type': 'atomic',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [{
                        'offset': 0,
                        'length': 1,
                        'key': 1
                    }],
                    'data': {},
                }
            ]
        }), '<ul class="steps"><li>test <a href="http://example.com"><code>style object</code> to <code>style string</code></a>).</li></ul><hr/>')

    def test_render_with_wrapping(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'dem5p',
                    'text': 'item2',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }
            ]
        }), '<ul class="steps"><li>item1</li><li>item2</li></ul>')

    def test_render_with_number_attribute(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {'length': 5},
                },
            }),
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<ul length="5"><li>item1</li></ul>')

    def test_render_with_boolean_attribute_true(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {'data-test': True},
                },
            }),
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<ul data-test="true"><li>item1</li></ul>')

    def test_render_with_boolean_attribute_false(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {'data-test': False},
                },
            }),
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ]
        }), '<ul data-test="false"><li>item1</li></ul>')

    def test_render_with_none_attribute(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {'data-test': None},
                },
            }),
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<ul><li>item1</li></ul>')

    def test_render_with_unknown_attribute(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {'*ngFor': 'test'},
                },
            })
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<ul *ngFor="test"><li>item1</li></ul>')

    def test_render_with_element_options(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.HEADER_TWO: {
                    'element': 'h2',
                    'props': {'class': 'c-amazing-heading'},
                },
            })
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'header-two',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<h2 class="c-amazing-heading">item1</h2>')

    def test_render_with_none_component(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNSTYLED: None,
            }),
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem12p',
                    'text': 'header',
                    'type': 'header-one',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'dem1p',
                    'text': 'paragraph',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<h1>header</h1>')

    def test_render_with_none_return_value(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNSTYLED: lambda props: None,
            }),
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem12p',
                    'text': 'header',
                    'type': 'header-one',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'dem1p',
                    'text': 'paragraph',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<h1>header</h1>')

    def test_render_with_entity(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {
                '2': {
                    'type': 'HORIZONTAL_RULE',
                    'mutability': 'IMMUTABLE',
                    'data': {},
                },
            },
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'dem5p',
                    'text': 'item2',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': '672oo',
                    'text': ' ',
                    'type': 'atomic',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [
                        {
                            'offset': 0,
                            'length': 1,
                            'key': 2,
                        },
                    ],
                },
            ]
        }), '<ul class="steps"><li>item1</li><li>item2</li></ul><hr/>')

    def test_render_with_wrapping_reset(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': '1',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '4ht9m',
                    'text': '2',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc4',
                    'text': '3',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc3',
                    'text': '4',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '3mn5b',
                    'text': '5',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<p>1</p><ul class="steps"><li>2</li></ul><p>3</p><ul class="steps"><li>4</li></ul><p>5</p>')

    def test_render_with_wrapping_reset_block_components(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': '1',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '4ht9m',
                    'text': '2',
                    'type': 'blockquote',
                    'depth': 0,
                    'data': {
                        'cite': '2'
                    },
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc4',
                    'text': '3',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc3',
                    'text': '4',
                    'type': 'blockquote',
                    'depth': 0,
                    'data': {
                        'cite': '4'
                    },
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '3mn5b',
                    'text': '5',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<p>1</p><div><blockquote cite="2">2</blockquote></div><p>3</p><div><blockquote cite="4">4</blockquote></div><p>5</p>')

    def test_render_with_unidirectional_nested_wrapping(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': 'A list item',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '4ht9m',
                    'text': 'Oops!',
                    'type': 'unordered-list-item',
                    'depth': 1,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc4',
                    'text': 'Does this support nesting?',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc3',
                    'text': 'Maybe?',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '3mn5b',
                    'text': 'Yep it does!',
                    'type': 'unordered-list-item',
                    'depth': 3,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '28umf',
                    'text': 'How many levels deep?',
                    'type': 'unordered-list-item',
                    'depth': 4,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'd81ns',
                    'text': 'Lots.',
                    'type': 'unordered-list-item',
                    'depth': 4,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'b0tsc',
                    'text': 'Ah.',
                    'type': 'unordered-list-item',
                    'depth': 4,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<ul class="steps"><li>A list item<ul class="steps"><li>Oops!<ul class="steps"><li>Does this support nesting?</li><li>Maybe?<ul class="steps"><li>Yep it does!<ul class="steps"><li>How many levels deep?</li><li>Lots.</li><li>Ah.</li></ul></li></ul></li></ul></li></ul></li></ul>')

    def test_render_with_backtracking_nested_wrapping(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': 'A list item (0)',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '4ht9m',
                    'text': 'Oops! (1)',
                    'type': 'unordered-list-item',
                    'depth': 1,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc4',
                    'text': 'Does this support nesting? (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc3',
                    'text': 'Maybe? (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '3mn5b',
                    'text': 'Yep it does! (3)',
                    'type': 'unordered-list-item',
                    'depth': 3,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '28umf',
                    'text': 'How many levels deep? (4)',
                    'type': 'unordered-list-item',
                    'depth': 4,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c2gc4',
                    'text': 'Backtracking, two at once... (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c1gcb',
                    'text': 'Uh oh (1)',
                    'type': 'unordered-list-item',
                    'depth': 1,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c2gh4',
                    'text': 'Up, up, and away! (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c1ghb',
                    'text': 'Arh! (1)',
                    'type': 'unordered-list-item',
                    'depth': 1,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c1gc9',
                    'text': 'Did this work? (0)',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c1gc9',
                    'text': 'Yes! (0)',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<ul class="steps"><li>A list item (0)<ul class="steps"><li>Oops! (1)<ul class="steps"><li>Does this support nesting? (2)</li><li>Maybe? (2)<ul class="steps"><li>Yep it does! (3)<ul class="steps"><li>How many levels deep? (4)</li></ul></li></ul></li><li>Backtracking, two at once... (2)</li></ul></li><li>Uh oh (1)<ul class="steps"><li>Up, up, and away! (2)</li></ul></li><li>Arh! (1)</li></ul></li><li>Did this work? (0)</li><li>Yes! (0)</li></ul>')

    def test_render_with_jumping_wrapping(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': 'A list item (0)',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '4ht9m',
                    'text': 'Jumps (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc4',
                    'text': 'Back (0)',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc3',
                    'text': 'Jumps again (3)',
                    'type': 'unordered-list-item',
                    'depth': 3,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '3mn5b',
                    'text': 'Back (1)',
                    'type': 'unordered-list-item',
                    'depth': 1,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<ul class="steps"><li>A list item (0)<ul class="steps"><li><ul class="steps"><li>Jumps (2)</li></ul></li></ul></li><li>Back (0)<ul class="steps"><li><ul class="steps"><li><ul class="steps"><li>Jumps again (3)</li></ul></li></ul></li><li>Back (1)</li></ul></li></ul>')

    def test_render_with_immediate_jumping(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': 'A list item (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '93agv',
                    'text': 'A list item (0)',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<ul class="steps"><li><ul class="steps"><li><ul class="steps"><li>A list item (2)</li></ul></li></ul></li><li>A list item (0)</li></ul>')

    def test_render_with_no_zero_depth(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': 'A list item (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '93agv',
                    'text': 'A list item (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<ul class="steps"><li><ul class="steps"><li><ul class="steps"><li>A list item (2)</li><li>A list item (2)</li></ul></li></ul></li></ul>')

    def test_render_with_big_content(self):
        self.assertEqual(HTML({
            'entity_decorators': {
                'LINK': link
            },
            'block_map': {
                'header-two': {'element': 'h2'},
                'blockquote': {'element': 'blockquote'},
                'unordered-list-item': {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {},
                },
                'unstyled': {'element': 'p'}
            },
            'style_map': {
                'ITALIC': {'element': 'em'},
                'BOLD': {'element': 'strong'}
            }
        }).render({
            'entityMap': {
                '0': {
                    'type': 'LINK',
                    'mutability': 'MUTABLE',
                    'data': {
                        'url': 'http://example.com'
                    }
                },
                '1': {
                    'type': 'LINK',
                    'mutability': 'MUTABLE',
                    'data': {
                        'url': 'https://www.springload.co.nz/work/nz-festival/'
                    }
                }
            },
            'blocks': [
                {
                    'key': '6mgfh',
                    'text': 'User experience (UX) design',
                    'type': 'header-two',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 16,
                            'length': 4,
                            'style': 'BOLD'
                        }
                    ],
                    'entityRanges': []
                },
                {
                    'key': '5384u',
                    'text': 'Everyone at Springload applies the best principles of UX to their work.',
                    'type': 'blockquote',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'eelkd',
                    'text': 'The design decisions we make building tools and services for your customers are based on empathy for what your customers need.',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'b9grk',
                    'text': 'User research',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'a1tis',
                    'text': 'User testing and analysis',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [
                        {
                            'offset': 0,
                            'length': 25,
                            'key': 0
                        }
                    ]
                },
                {
                    'key': 'adjdn',
                    'text': 'A/B testing',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': '62lio',
                    'text': 'Prototyping',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'fq3f',
                    'text': 'How we made it delightful and easy for people to find NZ Festival shows',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [
                        {
                            'offset': 0,
                            'length': 71,
                            'key': 1
                        }
                    ]
                }
            ]
        }), '<h2>User experience <strong>(UX)</strong> design</h2><blockquote>Everyone at Springload applies the best principles of UX to their work.</blockquote><p>The design decisions we make building tools and services for your customers are based on empathy for what your customers need.</p><ul><li>User research</li><li><a href="http://example.com">User testing and analysis</a></li><li>A/B testing</li><li>Prototyping</li></ul><p><a href="https://www.springload.co.nz/work/nz-festival/">How we made it delightful and easy for people to find NZ Festival shows</a></p>')

    def test_render_with_default_block_map(self):
        self.assertEqual(HTML({
            'style_map': {
                INLINE_STYLES.ITALIC: {'element': 'em'},
                INLINE_STYLES.BOLD: {'element': 'strong'},
                'HIGHLIGHT': {'element': 'strong', 'props': {'style': {'textDecoration': 'underline'}}},
            },
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 4,
                            'style': 'ITALIC'
                        }
                    ],
                    'entityRanges': []
                }
            ]
        }), '<p><em>some</em> paragraph text</p>')

    def test_render_with_default_style_map(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {'class': 'steps'},
                },
            })
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 4,
                            'style': 'ITALIC'
                        }
                    ],
                    'entityRanges': []
                }
            ]
        }), '<p><em>some</em> paragraph text</p>')

    def test_render_with_default_config(self):
        self.assertEqual(HTML().render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 4,
                            'style': 'ITALIC'
                        }
                    ],
                    'entityRanges': []
                }
            ]
        }), '<p><em>some</em> paragraph text</p>')

    def test_render_with_line_breaks(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text\nsplit in half',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 4,
                            'style': 'ITALIC'
                        }
                    ],
                    'entityRanges': []
                }
            ]
        }), '<p><em>some</em> paragraph text<br/>split in half</p>')

    def test_render_with_many_line_breaks(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': '\nsome paragraph text\nsplit in half\n',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 1,
                            'length': 4,
                            'style': 'ITALIC'
                        }
                    ],
                    'entityRanges': []
                }
            ]
        }), '<p><br/><em>some</em> paragraph text<br/>split in half<br/></p>')

    def test_render_with_entity_and_decorators(self):
        """
        The composite decorator should never render text in any entities.
        """
        self.assertEqual(self.exporter.render({
            'entityMap': {
                '1': {
                    'type': 'LINK',
                    'mutability': 'MUTABLE',
                    'data': {
                        'url': 'http://amazon.us'
                    }
                }
            },
            'blocks': [
                {
                    'key': '5s7g9',
                    'text': 'search http://a.us or https://yahoo.com or www.google.com for #github and #facebook',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [
                        {
                            'offset': 7,
                            'length': 11,
                            'key': 1
                        }
                    ],
                },
                {
                    'key': '34a12',
                    'text': '#check www.example.com',
                    'type': 'code-block',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ]
        }),
            '<p>search <a href="http://amazon.us">http://a.us</a> or '
            '<a href="https://yahoo.com">https://yahoo.com</a> or '
            '<a href="http://www.google.com">www.google.com</a> for '
            '<span class="hashtag">#github</span> and '
            '<span class="hashtag">#facebook</span></p>'
            '<pre><code>#check www.example.com</code></pre>')

    def test_render_with_multiple_decorators(self):
        """
        When multiple decorators match the same part of text,
        only the first one should perform the replacement.
        """
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '5s7g9',
                    'text': 'search http://www.google.com#world for the #world',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ]
        }),
            '<p>search <a href="http://www.google.com#world">'
            'http://www.google.com#world</a> for the '
            '<span class="hashtag">#world</span></p>')
Example #10
0
class TestHTML(unittest.TestCase):
    def setUp(self):
        self.exporter = HTML(config)

    def test_init(self):
        self.assertIsInstance(self.exporter, HTML)

    def test_render_block_exists(self):
        self.assertTrue('render_block' in dir(self.exporter))

    def test_build_style_commands_empty(self):
        self.assertEqual(
            str(
                self.exporter.build_style_commands({
                    'key': '5s7g9',
                    'text': 'Header',
                    'type': 'header-one',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                })), str([]))

    def test_build_style_commands_single(self):
        self.assertEqual(
            str(
                self.exporter.build_style_commands({
                    'key':
                    '5s7g9',
                    'text':
                    'Header',
                    'type':
                    'header-one',
                    'depth':
                    0,
                    'inlineStyleRanges': [{
                        'offset': 0,
                        'length': 4,
                        'style': 'ITALIC'
                    }],
                    'entityRanges': []
                })),
            str([
                Command('start_inline_style', 0, 'ITALIC'),
                Command('stop_inline_style', 4, 'ITALIC'),
            ]))

    def test_build_style_commands_multiple(self):
        self.assertEqual(
            str(
                self.exporter.build_style_commands({
                    'key':
                    '5s7g9',
                    'text':
                    'Header',
                    'type':
                    'header-one',
                    'depth':
                    0,
                    'inlineStyleRanges': [{
                        'offset': 0,
                        'length': 4,
                        'style': 'ITALIC'
                    }, {
                        'offset': 9,
                        'length': 3,
                        'style': 'BOLD'
                    }],
                    'entityRanges': []
                })),
            str([
                Command('start_inline_style', 0, 'ITALIC'),
                Command('stop_inline_style', 4, 'ITALIC'),
                Command('start_inline_style', 9, 'BOLD'),
                Command('stop_inline_style', 12, 'BOLD'),
            ]))

    def test_build_entity_commands_empty(self):
        self.assertEqual(
            str(
                self.exporter.build_entity_commands({
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                })), str([]))

    def test_build_entity_commands_single(self):
        self.assertEqual(
            str(
                self.exporter.build_entity_commands({
                    'key':
                    'dem5p',
                    'text':
                    'some paragraph text',
                    'type':
                    'unstyled',
                    'depth':
                    0,
                    'inlineStyleRanges': [],
                    'entityRanges': [{
                        'offset': 5,
                        'length': 9,
                        'key': 0
                    }]
                })),
            str([
                Command('start_entity', 5, 0),
                Command('stop_entity', 14, 0),
            ]))

    def test_build_entity_commands_multiple(self):
        self.assertEqual(
            str(
                self.exporter.build_entity_commands({
                    'key':
                    'dem5p',
                    'text':
                    'some paragraph text',
                    'type':
                    'unstyled',
                    'depth':
                    0,
                    'inlineStyleRanges': [],
                    'entityRanges': [{
                        'offset': 5,
                        'length': 9,
                        'key': 0
                    }, {
                        'offset': 0,
                        'length': 4,
                        'key': 1
                    }]
                })),
            str([
                Command('start_entity', 5, 0),
                Command('stop_entity', 14, 0),
                Command('start_entity', 0, 1),
                Command('stop_entity', 4, 1),
            ]))

    def test_build_commands_empty(self):
        self.assertEqual(
            str(
                self.exporter.build_commands({
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                })), str([
                    Command('start_text', 0),
                    Command('stop_text', 19),
                ]))

    def test_build_commands_multiple(self):
        self.assertEqual(
            str(
                self.exporter.build_commands({
                    'key':
                    'dem5p',
                    'text':
                    'some paragraph text',
                    'type':
                    'unstyled',
                    'depth':
                    0,
                    'inlineStyleRanges': [{
                        'offset': 0,
                        'length': 4,
                        'style': 'ITALIC'
                    }, {
                        'offset': 9,
                        'length': 3,
                        'style': 'BOLD'
                    }],
                    'entityRanges': [{
                        'offset': 5,
                        'length': 9,
                        'key': 0
                    }, {
                        'offset': 0,
                        'length': 4,
                        'key': 1
                    }]
                })),
            str([
                Command('start_text', 0),
                Command('stop_text', 19),
                Command('start_inline_style', 0, 'ITALIC'),
                Command('stop_inline_style', 4, 'ITALIC'),
                Command('start_inline_style', 9, 'BOLD'),
                Command('stop_inline_style', 12, 'BOLD'),
                Command('start_entity', 5, 0),
                Command('stop_entity', 14, 0),
                Command('start_entity', 0, 1),
                Command('stop_entity', 4, 1),
            ]))

    def test_build_command_groups_empty(self):
        self.assertEqual(
            str(
                self.exporter.build_command_groups({
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                })),
            str([
                ('some paragraph text', [Command('start_text', 0)]),
                ('', [Command('stop_text', 19)]),
            ]))

    def test_build_command_groups_multiple(self):
        self.assertEqual(
            str(
                self.exporter.build_command_groups({
                    'key':
                    'dem5p',
                    'text':
                    'some paragraph text',
                    'type':
                    'unstyled',
                    'depth':
                    0,
                    'inlineStyleRanges': [{
                        'offset': 0,
                        'length': 4,
                        'style': 'ITALIC'
                    }, {
                        'offset': 9,
                        'length': 3,
                        'style': 'BOLD'
                    }],
                    'entityRanges': [{
                        'offset': 5,
                        'length': 9,
                        'key': 0
                    }, {
                        'offset': 0,
                        'length': 4,
                        'key': 1
                    }]
                })),
            str([
                ('some', [
                    Command('start_text', 0),
                    Command('start_inline_style', 0, 'ITALIC'),
                    Command('start_entity', 0, 1),
                ]),
                (' ', [
                    Command('stop_inline_style', 4, 'ITALIC'),
                    Command('stop_entity', 4, 1),
                ]),
                ('para', [
                    Command('start_entity', 5, 0),
                ]),
                ('gra', [
                    Command('start_inline_style', 9, 'BOLD'),
                ]),
                ('ph', [
                    Command('stop_inline_style', 12, 'BOLD'),
                ]),
                (' text', [
                    Command('stop_entity', 14, 0),
                ]),
                ('', [Command('stop_text', 19)]),
            ]))

    def test_render(self):
        self.assertEqual(
            self.exporter.render({
                'entityMap': {},
                'blocks': [
                    {
                        'key': '5s7g9',
                        'text': 'Header',
                        'type': 'header-one',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': []
                    },
                ]
            }), '<h1>Header</h1>')

    def test_render_twice(self):
        """Asserts no state is kept during renders."""
        self.assertEqual(
            self.exporter.render({
                'entityMap': {},
                'blocks': [
                    {
                        'key': '5s7g9',
                        'text': 'Header',
                        'type': 'header-one',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': []
                    },
                ]
            }), '<h1>Header</h1>')
        self.assertEqual(
            self.exporter.render({
                'entityMap': {},
                'blocks': [
                    {
                        'key': '5s7g9',
                        'text': 'Header',
                        'type': 'header-one',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': []
                    },
                ]
            }), '<h1>Header</h1>')
Example #11
0
            "data": {}
        }, {
            "key": "1nols",
            "text": "Voilà!",
            "type": "unstyled",
            "depth": 0,
            "inlineStyleRanges": [],
            "entityRanges": [],
            "data": {}
        }]
    }

    pr = cProfile.Profile()
    pr.enable()

    markup = exporter.render(content_state)

    pr.disable()
    p = Stats(pr)

    def prettify(markup):
        return re.sub(r'</?(body|html|head)>', '', BeautifulSoup(markup, 'html5lib').prettify()).strip()

    pretty = prettify(markup)

    # Display in console.
    print(pretty)

    p.strip_dirs().sort_stats('cumulative').print_stats(0)

    styles = """
Example #12
0
            "data": {}
        }, {
            "key": "1nols",
            "text": "Voilà!",
            "type": "unstyled",
            "depth": 0,
            "inlineStyleRanges": [],
            "entityRanges": [],
            "data": {}
        }]
    }

    pr = cProfile.Profile()
    pr.enable()

    markup = exporter.render(content_state)

    pr.disable()
    p = Stats(pr)

    def prettify(markup):
        return re.sub(r'</?(body|html|head)>', '',
                      BeautifulSoup(markup, 'html5lib').prettify()).strip()

    pretty = prettify(markup)

    # Display in console.
    print(pretty)

    p.strip_dirs().sort_stats('cumulative').print_stats(0)
Example #13
0
    def _parse_editor_state(self, article, ninjs):
        """Parse editor_state (DraftJs internals) to retrieve annotations

        body_html will be rewritten with HTML generated from DraftJS representation
        and annotation will be included in <span> elements
        :param article: item to modify, must contain "editor_state" data
        :param ninjs: ninjs item which will be formatted
        """
        blocks = article['editor_state']['blocks']
        blocks_map = {}
        ann_idx = 0
        data = {}
        config = {
            'engine': 'lxml',
            'entity_decorators': {
                ENTITY_TYPES.LINK:
                self._render_link,
                ENTITY_TYPES.HORIZONTAL_RULE:
                lambda props: DOM.create_element('hr'),
                ENTITY_TYPES.EMBED:
                self._render_embed,
                MEDIA:
                self._render_media,
                ANNOTATION:
                self._render_annotation
            }
        }
        renderer = HTML(config)

        for block in blocks:
            blocks_map[block['key']] = block
            data.update(block['data'])

        # we sort data keys to have consistent annotations ids
        for key in sorted(data):
            data_block = data[key]
            if data_block['type'] == ANNOTATION:
                ninjs.setdefault('annotations', []).append({
                    'id':
                    ann_idx,
                    'type':
                    data_block['annotationType'],
                    'body':
                    renderer.render(json.loads(data_block['msg']))
                })
                entity_key = '_annotation_{}'.format(ann_idx)
                article['editor_state']['entityMap'][entity_key] = {
                    'type': ANNOTATION,
                    'data': {
                        'id': ann_idx
                    }
                }
                ann_idx += 1
                selection = json.loads(key)
                if selection['isBackward']:
                    first, second = 'focus', 'anchor'
                else:
                    first, second = 'anchor', 'focus'
                first_key = selection[first + 'Key']
                second_key = selection[second + 'Key']
                first_offset = selection[first + 'Offset']
                second_offset = selection[second + 'Offset']
                # we want to style annotation with <span>, so we put them as entities
                if first_key == second_key:
                    # selection is done in a single block
                    annotated_block = blocks_map[first_key]
                    annotated_block.setdefault('entityRanges', []).append({
                        'key':
                        entity_key,
                        'offset':
                        first_offset,
                        'length':
                        second_offset - first_offset
                    })
                else:
                    # selection is done on multiple blocks, we have to select them
                    started = False
                    for block in blocks:
                        if block['key'] == first_key:
                            started = True
                            block.setdefault('entityRanges', []).append({
                                'key':
                                entity_key,
                                'offset':
                                first_offset,
                                'length':
                                len(block['text']) - first_offset
                            })
                        elif started:
                            inline = {'key': entity_key, 'offset': 0}
                            block.setdefault('entityRanges', []).append(inline)
                            if block['key'] == second_key:
                                # last block, we end the annotation here
                                inline['length'] = second_offset
                                break
                            else:
                                # intermediate block, we annotate it whole
                                inline['length'] = len(block['text'])
        # HTML rendering
        # now we have annotation ready, we can render HTML
        # we change body_html if and only if we have annotations to render
        if ninjs.get('annotations'):
            article['body_html'] = renderer.render(article['editor_state'])
Example #14
0
}

exporter = HTML(config)

content_states = get_content_sample()

BENCHMARK_RUNS = int(os.environ.get('BENCHMARK_RUNS', 1))

print('Exporting %s ContentStates %s times' % (len(content_states), BENCHMARK_RUNS))

pr = cProfile.Profile()
pr.enable()

for i in range(0, BENCHMARK_RUNS):
    for content_state in content_states:
        exporter.render(content_state)

pr.disable()
p = Stats(pr)

p.strip_dirs().sort_stats('cumulative').print_stats(10)

print('Measuring memory consumption')


@profile(precision=6)
def memory_consumption_run():
    exporter = HTML(config)

    for content_state in content_states:
        exporter.render(content_state)
class TestOutput(unittest.TestCase):
    """
    Test cases related to specific features of the HTML builder.
    """

    def setUp(self):
        self.maxDiff = None
        self.exporter = HTML(config)

    def test_render_empty(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': []
        }), '')

    def test_render_with_different_blocks(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '5s7g9',
                    'text': 'Header',
                    'type': 'header-one',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }
            ]
        }), '<h1>Header</h1><p>some paragraph text</p>')

    def test_render_with_unicode(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'Emojis! 🍺',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }
            ]
        }), '<p>Emojis! \U0001f37a</p>')

    def test_render_with_inline_styles(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 4,
                            'style': 'ITALIC'
                        }
                    ],
                    'entityRanges': []
                }
            ]
        }), '<p><em>some</em> paragraph text</p>')

    def test_render_with_multiple_inline_styles(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {
                '0': {
                    'type': 'LINK',
                    'mutability': 'MUTABLE',
                    'data': {
                        'url': 'http://example.com'
                    }
                }
            },
            'blocks': [
                {
                    'key': '5s7g9',
                    'text': 'Header',
                    'type': 'header-one',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 2,
                            'style': 'BOLD'
                        }
                    ],
                    'entityRanges': []
                },
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 4,
                            'style': 'HIGHLIGHT'
                        }
                    ],
                    'entityRanges': [
                        {
                            'offset': 5,
                            'length': 9,
                            'key': 0
                        }
                    ]
                }
            ]
        }), '<h1><strong>He</strong>ader</h1><p><strong style="text-decoration: underline;">some</strong> <a href="http://example.com">paragraph</a> text</p>')

    def test_render_with_entities(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {
                '0': {
                    'type': 'LINK',
                    'mutability': 'MUTABLE',
                    'data': {
                        'url': 'http://example.com'
                    }
                }
            },
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [
                        {
                            'offset': 5,
                            'length': 9,
                            'key': 0
                        }
                    ]
                }
            ]
        }), '<p>some <a href="http://example.com">paragraph</a> text</p>')

    def test_render_with_entities_crossing_raises(self):
        with self.assertRaises(EntityException):
            self.exporter.render({
                'entityMap': {
                    '0': {
                        'type': 'LINK',
                        'mutability': 'MUTABLE',
                        'data': {
                            'url': 'http://example.com'
                        }
                    },
                    '1': {
                        'type': 'LINK',
                        'mutability': 'MUTABLE',
                        'data': {
                            'url': 'http://bar.example.com'
                        }
                    }
                },
                'blocks': [
                    {
                        'key': 'dem5p',
                        'text': 'some paragraph text',
                        'type': 'unstyled',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': [
                            {
                                'offset': 5,
                                'length': 9,
                                'key': 0
                            },
                            {
                                'offset': 2,
                                'length': 9,
                                'key': 1
                            }
                        ]
                    }
                ]
            })

    def test_render_with_styles_in_entities(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {
                '0': {
                    'type': 'LINK',
                    'mutability': 'MUTABLE',
                    'data': {
                        'url': 'http://example.com'
                    }
                },
                '1': {
                    'type': 'HORIZONTAL_RULE',
                    'mutability': 'IMMUTABLE',
                    'data': {},
                },
            },
            'blocks': [
                {
                    'key': 'f4gp0',
                    'text': 'test style object to style string).',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 5,
                            'length': 12,
                            'style': 'CODE',
                        },
                        {
                            'offset': 21,
                            'length': 12,
                            'style': 'CODE',
                        }
                    ],
                    'entityRanges': [
                        {
                            'offset': 5,
                            'length': 28,
                            'key': 0
                        },
                    ],
                    'data': {},
                }, {
                    'key': 'f4fp0',
                    'text': ' ',
                    'type': 'atomic',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [{
                        'offset': 0,
                        'length': 1,
                        'key': 1
                    }],
                    'data': {},
                }
            ]
        }), '<ul class="steps"><li>test <a href="http://example.com"><code>style object</code> to <code>style string</code></a>).</li></ul><hr/>')

    def test_render_with_wrapping(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'dem5p',
                    'text': 'item2',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }
            ]
        }), '<ul class="steps"><li>item1</li><li>item2</li></ul>')

    def test_render_with_number_attribute(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {'length': 5},
                },
            }),
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<ul length="5"><li>item1</li></ul>')

    def test_render_with_boolean_attribute_true(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {'data-test': True},
                },
            }),
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<ul data-test="true"><li>item1</li></ul>')

    def test_render_with_boolean_attribute_false(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {'data-test': False},
                },
            }),
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ]
        }), '<ul data-test="false"><li>item1</li></ul>')

    def test_render_with_none_attribute(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {'data-test': None},
                },
            }),
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<ul><li>item1</li></ul>')

    def test_render_with_unknown_attribute(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {'*ngFor': 'test'},
                },
            })
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<ul *ngFor="test"><li>item1</li></ul>')

    def test_render_with_element_options(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.HEADER_TWO: {
                    'element': 'h2',
                    'props': {'class': 'c-amazing-heading'},
                },
            })
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'header-two',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<h2 class="c-amazing-heading">item1</h2>')

    def test_render_with_none_component(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNSTYLED: None,
            }),
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem12p',
                    'text': 'header',
                    'type': 'header-one',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'dem1p',
                    'text': 'paragraph',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<h1>header</h1>')

    def test_render_with_none_return_value(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNSTYLED: lambda props: None,
            }),
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem12p',
                    'text': 'header',
                    'type': 'header-one',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'dem1p',
                    'text': 'paragraph',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ],
        }), '<h1>header</h1>')

    def test_render_with_entity(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {
                '2': {
                    'type': 'HORIZONTAL_RULE',
                    'mutability': 'IMMUTABLE',
                    'data': {},
                },
            },
            'blocks': [
                {
                    'key': 'dem1p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'dem5p',
                    'text': 'item2',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': '672oo',
                    'text': ' ',
                    'type': 'atomic',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [
                        {
                            'offset': 0,
                            'length': 1,
                            'key': 2,
                        },
                    ],
                },
            ]
        }), '<ul class="steps"><li>item1</li><li>item2</li></ul><hr/>')

    def test_render_with_wrapping_reset(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': '1',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '4ht9m',
                    'text': '2',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc4',
                    'text': '3',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc3',
                    'text': '4',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '3mn5b',
                    'text': '5',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<p>1</p><ul class="steps"><li>2</li></ul><p>3</p><ul class="steps"><li>4</li></ul><p>5</p>')

    def test_render_with_wrapping_reset_block_components(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': '1',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '4ht9m',
                    'text': '2',
                    'type': 'blockquote',
                    'depth': 0,
                    'data': {
                        'cite': '2'
                    },
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc4',
                    'text': '3',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc3',
                    'text': '4',
                    'type': 'blockquote',
                    'depth': 0,
                    'data': {
                        'cite': '4'
                    },
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '3mn5b',
                    'text': '5',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<p>1</p><div><blockquote cite="2">2</blockquote></div><p>3</p><div><blockquote cite="4">4</blockquote></div><p>5</p>')

    def test_render_with_unidirectional_nested_wrapping(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': 'A list item',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '4ht9m',
                    'text': 'Oops!',
                    'type': 'unordered-list-item',
                    'depth': 1,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc4',
                    'text': 'Does this support nesting?',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc3',
                    'text': 'Maybe?',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '3mn5b',
                    'text': 'Yep it does!',
                    'type': 'unordered-list-item',
                    'depth': 3,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '28umf',
                    'text': 'How many levels deep?',
                    'type': 'unordered-list-item',
                    'depth': 4,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'd81ns',
                    'text': 'Lots.',
                    'type': 'unordered-list-item',
                    'depth': 4,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'b0tsc',
                    'text': 'Ah.',
                    'type': 'unordered-list-item',
                    'depth': 4,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<ul class="steps"><li>A list item<ul class="steps"><li>Oops!<ul class="steps"><li>Does this support nesting?</li><li>Maybe?<ul class="steps"><li>Yep it does!<ul class="steps"><li>How many levels deep?</li><li>Lots.</li><li>Ah.</li></ul></li></ul></li></ul></li></ul></li></ul>')

    def test_render_with_backtracking_nested_wrapping(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': 'A list item (0)',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '4ht9m',
                    'text': 'Oops! (1)',
                    'type': 'unordered-list-item',
                    'depth': 1,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc4',
                    'text': 'Does this support nesting? (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc3',
                    'text': 'Maybe? (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '3mn5b',
                    'text': 'Yep it does! (3)',
                    'type': 'unordered-list-item',
                    'depth': 3,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '28umf',
                    'text': 'How many levels deep? (4)',
                    'type': 'unordered-list-item',
                    'depth': 4,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c2gc4',
                    'text': 'Backtracking, two at once... (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c1gcb',
                    'text': 'Uh oh (1)',
                    'type': 'unordered-list-item',
                    'depth': 1,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c2gh4',
                    'text': 'Up, up, and away! (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c1ghb',
                    'text': 'Arh! (1)',
                    'type': 'unordered-list-item',
                    'depth': 1,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c1gc9',
                    'text': 'Did this work? (0)',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c1gc9',
                    'text': 'Yes! (0)',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<ul class="steps"><li>A list item (0)<ul class="steps"><li>Oops! (1)<ul class="steps"><li>Does this support nesting? (2)</li><li>Maybe? (2)<ul class="steps"><li>Yep it does! (3)<ul class="steps"><li>How many levels deep? (4)</li></ul></li></ul></li><li>Backtracking, two at once... (2)</li></ul></li><li>Uh oh (1)<ul class="steps"><li>Up, up, and away! (2)</li></ul></li><li>Arh! (1)</li></ul></li><li>Did this work? (0)</li><li>Yes! (0)</li></ul>')

    def test_render_with_jumping_wrapping(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': 'A list item (0)',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '4ht9m',
                    'text': 'Jumps (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc4',
                    'text': 'Back (0)',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': 'c6gc3',
                    'text': 'Jumps again (3)',
                    'type': 'unordered-list-item',
                    'depth': 3,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '3mn5b',
                    'text': 'Back (1)',
                    'type': 'unordered-list-item',
                    'depth': 1,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<ul class="steps"><li>A list item (0)<ul class="steps"><li><ul class="steps"><li>Jumps (2)</li></ul></li></ul></li><li>Back (0)<ul class="steps"><li><ul class="steps"><li><ul class="steps"><li>Jumps again (3)</li></ul></li></ul></li><li>Back (1)</li></ul></li></ul>')

    def test_render_with_immediate_jumping(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': 'A list item (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '93agv',
                    'text': 'A list item (0)',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<ul class="steps"><li><ul class="steps"><li><ul class="steps"><li>A list item (2)</li></ul></li></ul></li><li>A list item (0)</li></ul>')

    def test_render_with_no_zero_depth(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '93agv',
                    'text': 'A list item (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
                {
                    'key': '93agv',
                    'text': 'A list item (2)',
                    'type': 'unordered-list-item',
                    'depth': 2,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ],
        }), '<ul class="steps"><li><ul class="steps"><li><ul class="steps"><li>A list item (2)</li><li>A list item (2)</li></ul></li></ul></li></ul>')

    def test_render_with_big_content(self):
        self.assertEqual(HTML({
            'entity_decorators': {
                'LINK': link
            },
            'block_map': {
                'header-two': {'element': 'h2'},
                'blockquote': {'element': 'blockquote'},
                'unordered-list-item': {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {},
                },
                'unstyled': {'element': 'p'}
            },
            'style_map': {
                'ITALIC': {'element': 'em'},
                'BOLD': {'element': 'strong'}
            }
        }).render({
            'entityMap': {
                '0': {
                    'type': 'LINK',
                    'mutability': 'MUTABLE',
                    'data': {
                        'url': 'http://example.com'
                    }
                },
                '1': {
                    'type': 'LINK',
                    'mutability': 'MUTABLE',
                    'data': {
                        'url': 'https://www.springload.co.nz/work/nz-festival/'
                    }
                }
            },
            'blocks': [
                {
                    'key': '6mgfh',
                    'text': 'User experience (UX) design',
                    'type': 'header-two',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 16,
                            'length': 4,
                            'style': 'BOLD'
                        }
                    ],
                    'entityRanges': []
                },
                {
                    'key': '5384u',
                    'text': 'Everyone at Springload applies the best principles of UX to their work.',
                    'type': 'blockquote',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'eelkd',
                    'text': 'The design decisions we make building tools and services for your customers are based on empathy for what your customers need.',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'b9grk',
                    'text': 'User research',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'a1tis',
                    'text': 'User testing and analysis',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [
                        {
                            'offset': 0,
                            'length': 25,
                            'key': 0
                        }
                    ]
                },
                {
                    'key': 'adjdn',
                    'text': 'A/B testing',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': '62lio',
                    'text': 'Prototyping',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
                {
                    'key': 'fq3f',
                    'text': 'How we made it delightful and easy for people to find NZ Festival shows',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [
                        {
                            'offset': 0,
                            'length': 71,
                            'key': 1
                        }
                    ]
                }
            ]
        }), '<h2>User experience <strong>(UX)</strong> design</h2><blockquote>Everyone at Springload applies the best principles of UX to their work.</blockquote><p>The design decisions we make building tools and services for your customers are based on empathy for what your customers need.</p><ul><li>User research</li><li><a href="http://example.com">User testing and analysis</a></li><li>A/B testing</li><li>Prototyping</li></ul><p><a href="https://www.springload.co.nz/work/nz-festival/">How we made it delightful and easy for people to find NZ Festival shows</a></p>')

    def test_render_with_default_block_map(self):
        self.assertEqual(HTML({
            'style_map': {
                INLINE_STYLES.ITALIC: {'element': 'em'},
                INLINE_STYLES.BOLD: {'element': 'strong'},
                'HIGHLIGHT': {'element': 'strong', 'textDecoration': 'underline'},
            },
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 4,
                            'style': 'ITALIC'
                        }
                    ],
                    'entityRanges': []
                }
            ]
        }), '<p><em>some</em> paragraph text</p>')

    def test_render_with_default_style_map(self):
        self.assertEqual(HTML({
            'block_map': dict(BLOCK_MAP, **{
                BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                    'element': 'li',
                    'wrapper': 'ul',
                    'wrapper_props': {'class': 'steps'},
                },
            })
        }).render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 4,
                            'style': 'ITALIC'
                        }
                    ],
                    'entityRanges': []
                }
            ]
        }), '<p><em>some</em> paragraph text</p>')

    def test_render_with_default_config(self):
        self.assertEqual(HTML().render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 4,
                            'style': 'ITALIC'
                        }
                    ],
                    'entityRanges': []
                }
            ]
        }), '<p><em>some</em> paragraph text</p>')

    def test_render_with_line_breaks(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': 'some paragraph text\nsplit in half',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 0,
                            'length': 4,
                            'style': 'ITALIC'
                        }
                    ],
                    'entityRanges': []
                }
            ]
        }), '<p><em>some</em> paragraph text<br/>split in half</p>')

    def test_render_with_many_line_breaks(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': 'dem5p',
                    'text': '\nsome paragraph text\nsplit in half\n',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [
                        {
                            'offset': 1,
                            'length': 4,
                            'style': 'ITALIC'
                        }
                    ],
                    'entityRanges': []
                }
            ]
        }), '<p><br/><em>some</em> paragraph text<br/>split in half<br/></p>')

    def test_render_with_entity_and_decorators(self):
        """
        The composite decorator should never render text in any entities.
        """
        self.assertEqual(self.exporter.render({
            'entityMap': {
                '1': {
                    'type': 'LINK',
                    'mutability': 'MUTABLE',
                    'data': {
                        'url': 'http://amazon.us'
                    }
                }
            },
            'blocks': [
                {
                    'key': '5s7g9',
                    'text': 'search http://a.us or https://yahoo.com or www.google.com for #github and #facebook',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [
                        {
                            'offset': 7,
                            'length': 11,
                            'key': 1
                        }
                    ],
                },
                {
                    'key': '34a12',
                    'text': '#check www.example.com',
                    'type': 'code-block',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ]
        }),
            '<p>search <a href="http://amazon.us">http://a.us</a> or '
            '<a href="https://yahoo.com">https://yahoo.com</a> or '
            '<a href="http://www.google.com">www.google.com</a> for '
            '<span class="hashtag">#github</span> and '
            '<span class="hashtag">#facebook</span></p>'
            '<pre><code>#check www.example.com</code></pre>')

    def test_render_with_multiple_decorators(self):
        """
        When multiple decorators match the same part of text,
        only the first one should perform the replacement.
        """
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '5s7g9',
                    'text': 'search http://www.google.com#world for the #world',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                },
            ]
        }),
            '<p>search <a href="http://www.google.com#world">'
            'http://www.google.com#world</a> for the '
            '<span class="hashtag">#world</span></p>')
Example #16
0
class TestHTML(unittest.TestCase):
    def setUp(self):
        self.exporter = HTML(config)

    def test_init(self):
        self.assertIsInstance(self.exporter, HTML)

    def test_init_dom_engine_default(self):
        HTML()
        self.assertEqual(DOM.dom, DOMString)

    def test_render_block_exists(self):
        self.assertTrue('render_block' in dir(self.exporter))

    def test_build_style_commands_empty(self):
        self.assertEqual(str(self.exporter.build_style_commands({
            'key': '5s7g9',
            'text': 'Header',
            'type': 'header-one',
            'depth': 0,
            'inlineStyleRanges': [],
            'entityRanges': []
        })), str([]))

    def test_build_style_commands_single(self):
        self.assertEqual(str(self.exporter.build_style_commands({
            'key': '5s7g9',
            'text': 'Header',
            'type': 'header-one',
            'depth': 0,
            'inlineStyleRanges': [
                {
                    'offset': 0,
                    'length': 4,
                    'style': 'ITALIC'
                }
            ],
            'entityRanges': []
        })), str([
            Command('start_inline_style', 0, 'ITALIC'),
            Command('stop_inline_style', 4, 'ITALIC'),
        ]))

    def test_build_style_commands_multiple(self):
        self.assertEqual(str(self.exporter.build_style_commands({
            'key': '5s7g9',
            'text': 'Header',
            'type': 'header-one',
            'depth': 0,
            'inlineStyleRanges': [
                {
                    'offset': 0,
                    'length': 4,
                    'style': 'ITALIC'
                },
                {
                    'offset': 9,
                    'length': 3,
                    'style': 'BOLD'
                }
            ],
            'entityRanges': []
        })), str([
            Command('start_inline_style', 0, 'ITALIC'),
            Command('stop_inline_style', 4, 'ITALIC'),
            Command('start_inline_style', 9, 'BOLD'),
            Command('stop_inline_style', 12, 'BOLD'),
        ]))

    def test_build_entity_commands_empty(self):
        self.assertEqual(str(self.exporter.build_entity_commands({
            'key': 'dem5p',
            'text': 'some paragraph text',
            'type': 'unstyled',
            'depth': 0,
            'inlineStyleRanges': [],
            'entityRanges': []
        })), str([]))

    def test_build_entity_commands_single(self):
        self.assertEqual(str(self.exporter.build_entity_commands({
            'key': 'dem5p',
            'text': 'some paragraph text',
            'type': 'unstyled',
            'depth': 0,
            'inlineStyleRanges': [],
            'entityRanges': [
                {
                    'offset': 5,
                    'length': 9,
                    'key': 0
                }
            ]
        })), str([
            Command('start_entity', 5, 0),
            Command('stop_entity', 14, 0),
        ]))

    def test_build_entity_commands_multiple(self):
        self.assertEqual(str(self.exporter.build_entity_commands({
            'key': 'dem5p',
            'text': 'some paragraph text',
            'type': 'unstyled',
            'depth': 0,
            'inlineStyleRanges': [],
            'entityRanges': [
                {
                    'offset': 5,
                    'length': 9,
                    'key': 0
                },
                {
                    'offset': 0,
                    'length': 4,
                    'key': 1
                }
            ]
        })), str([
            Command('start_entity', 5, 0),
            Command('stop_entity', 14, 0),
            Command('start_entity', 0, 1),
            Command('stop_entity', 4, 1),
        ]))

    def test_build_commands_empty(self):
        self.assertEqual(str(self.exporter.build_commands({
            'key': 'dem5p',
            'text': 'some paragraph text',
            'type': 'unstyled',
            'depth': 0,
            'inlineStyleRanges': [],
            'entityRanges': []
        })), str([
            Command('start_text', 0),
            Command('stop_text', 19),
        ]))

    def test_build_commands_multiple(self):
        self.assertEqual(str(self.exporter.build_commands({
            'key': 'dem5p',
            'text': 'some paragraph text',
            'type': 'unstyled',
            'depth': 0,
            'inlineStyleRanges': [
                {
                    'offset': 0,
                    'length': 4,
                    'style': 'ITALIC'
                },
                {
                    'offset': 9,
                    'length': 3,
                    'style': 'BOLD'
                }
            ],
            'entityRanges': [
                {
                    'offset': 5,
                    'length': 9,
                    'key': 0
                },
                {
                    'offset': 0,
                    'length': 4,
                    'key': 1
                }
            ]
        })), str([
            Command('start_text', 0),
            Command('stop_text', 19),
            Command('start_inline_style', 0, 'ITALIC'),
            Command('stop_inline_style', 4, 'ITALIC'),
            Command('start_inline_style', 9, 'BOLD'),
            Command('stop_inline_style', 12, 'BOLD'),
            Command('start_entity', 5, 0),
            Command('stop_entity', 14, 0),
            Command('start_entity', 0, 1),
            Command('stop_entity', 4, 1),
        ]))

    def test_build_command_groups_empty(self):
        self.assertEqual(str(self.exporter.build_command_groups({
            'key': 'dem5p',
            'text': 'some paragraph text',
            'type': 'unstyled',
            'depth': 0,
            'inlineStyleRanges': [],
            'entityRanges': []
        })), str([
            ('some paragraph text', [
                Command('start_text', 0),
            ]),
            ('', [
                Command('stop_text', 19),
            ])
        ]))

    def test_build_command_groups_multiple(self):
        self.assertEqual(str(self.exporter.build_command_groups({
            'key': 'dem5p',
            'text': 'some paragraph text',
            'type': 'unstyled',
            'depth': 0,
            'inlineStyleRanges': [
                {
                    'offset': 0,
                    'length': 4,
                    'style': 'ITALIC'
                },
                {
                    'offset': 9,
                    'length': 3,
                    'style': 'BOLD'
                }
            ],
            'entityRanges': [
                {
                    'offset': 5,
                    'length': 9,
                    'key': 0
                },
                {
                    'offset': 0,
                    'length': 4,
                    'key': 1
                }
            ]
        })), str([
            ('some', [
                Command('start_text', 0),
                Command('start_inline_style', 0, 'ITALIC'),
                Command('start_entity', 0, 1),
            ]),
            (' ', [
                Command('stop_inline_style', 4, 'ITALIC'),
                Command('stop_entity', 4, 1),
            ]),
            ('para', [
                Command('start_entity', 5, 0),
            ]),
            ('gra', [
                Command('start_inline_style', 9, 'BOLD'),
            ]),
            ('ph', [
                Command('stop_inline_style', 12, 'BOLD'),
            ]),
            (' text', [
                Command('stop_entity', 14, 0),
            ]),
            ('', [
                Command('stop_text', 19),
            ])
        ]))

    def test_render(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '5s7g9',
                    'text': 'Header',
                    'type': 'header-one',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ]
        }), '<h1>Header</h1>')

    def test_render_empty(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
            ]
        }), '')

    def test_render_none(self):
        self.assertEqual(self.exporter.render(None), '')

    def test_render_twice(self):
        """Asserts no state is kept during renders."""
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '5s7g9',
                    'text': 'Header',
                    'type': 'header-one',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ]
        }), '<h1>Header</h1>')
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': [
                {
                    'key': '5s7g9',
                    'text': 'Header',
                    'type': 'header-one',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                },
            ]
        }), '<h1>Header</h1>')
Example #17
0
class DraftJSHTMLExporter:
    def __init__(self, content_editor):
        self.content_editor = content_editor
        # XXX: we need one exporter per DraftJSHTMLExporter because
        #      self.render_media needs to access "entityMap" from local
        #      instance. If MEDIA rendering changes in the future, exporter
        #      should be global for all DraftJSHTMLExporter instances
        self.exporter = HTML({
            "engine":
            DOM.LXML,
            "style_map":
            dict(
                STYLE_MAP,
                **{
                    INLINE_STYLES.BOLD: "b",
                    INLINE_STYLES.ITALIC: "i",
                    INLINE_STYLES.FALLBACK: self.style_fallback,
                },
            ),
            "entity_decorators": {
                ENTITY_TYPES.LINK:
                self.render_link,
                ENTITY_TYPES.HORIZONTAL_RULE:
                lambda props: DOM.create_element("hr"),
                ENTITY_TYPES.EMBED:
                self.render_embed,
                MEDIA:
                self.render_media,
                ANNOTATION:
                self.render_annotation,
                TABLE:
                self.render_table,
            },
        })

    @property
    def content_state(self):
        return self.content_editor.content_state

    def render(self):
        blocks = self.content_state["blocks"]
        if not blocks:
            return ""
        if blocks[0].get("text", "-").strip() == "" or blocks[-1].get(
                "text", "-").strip() == "":
            # first and last block may be empty due to client constraints, in this case
            # we must discard them during rendering
            content_state = self.content_state.copy()
            content_state["blocks"] = blocks = blocks[:]
            if blocks[0]["text"].strip(
            ) == "" and not blocks[0]["entityRanges"]:
                del blocks[0]
            if blocks and blocks[-1]["text"].strip(
            ) == "" and not blocks[-1]["entityRanges"]:
                del blocks[-1]
        else:
            content_state = self.content_state

        try:
            for block in content_state["blocks"]:
                block.setdefault("depth", 0)
                block.setdefault("entityRanges", [])
                block.setdefault("inlineStyleRanges", [])
            html = self.exporter.render(content_state)
        except KeyError as e:
            if e.args == ("text", ):
                # "text" may be missing in some case (e.g. comments), and the exporter
                # doesn't support it. To avoid a crash, we render again after
                # filtering out all block elements without "text".
                content_state = self.content_state.copy()
                content_state["blocks"] = [
                    b for b in content_state["blocks"] if "text" in b
                ]
                html = self.exporter.render(content_state)
            else:
                raise e
        # see render_media for details
        return DUMMY_RE.sub("", html)

    def render_annotation(self, props):
        return props["children"]

    def render_media(self, props):
        media_props = props["media"]
        media_type = media_props.get("type", "picture")
        rendition = media_props["renditions"].get(
            "original") or media_props["renditions"]["viewImage"]
        alt_text = media_props.get("alt_text") or ""
        desc = media_props.get("description_text")
        if media_type == "picture":
            embed_type = "Image"
            elt = DOM.create_element("img", {
                "src": rendition["href"],
                "alt": alt_text
            }, props["children"])
        elif media_type == "video":
            embed_type = "Video"
            elt = DOM.create_element(
                "video",
                {
                    "control": "control",
                    "src": rendition["href"],
                    "alt": alt_text,
                    "width": "100%",
                    "height": "100%"
                },
                props["children"],
            )
        elif media_type == "audio":
            embed_type = "Audio"
            elt = DOM.create_element(
                "audio",
                {
                    "control": "control",
                    "src": rendition["href"],
                    "alt": alt_text,
                    "width": "100%",
                    "height": "100%"
                },
                props["children"],
            )
        else:
            logger.error(
                "Invalid or not implemented media type: {media_type}".format(
                    media_type=media_type))
            return None

        content = DOM.render(elt)

        if desc:
            content += "<figcaption>{}</figcaption>".format(desc)

        # we need to retrieve the key, there is not straightforward way to do it
        # so we find the key in entityMap with a corresponding value
        embed_key = next(k for k, v in self.content_state["entityMap"].items()
                         if v["data"].get("media") == props["media"])

        # <dummy_tag> is needed for the comments, because a root node is necessary
        # it will be removed during rendering.
        embed = DOM.parse_html(
            dedent("""\
            <dummy_tag><!-- EMBED START {embed_type} {{id: "editor_{key}"}} -->
            <figure>{content}</figure>
            <!-- EMBED END {embed_type} {{id: "editor_{key}"}} --></dummy_tag>"""
                   ).format(embed_type=embed_type,
                            key=embed_key,
                            content=content))

        return embed

    def render_link(self, props):
        if "url" in props:
            attribs = {"href": props["url"]}
        else:
            link_data = props.get("link", {})
            if link_data.get("attachment"):
                attribs = {"data-attachment": link_data["attachment"]}
            elif link_data.get("target"):
                attribs = {
                    "href": link_data["href"],
                    "target": link_data["target"]
                }
            else:
                attribs = {"href": link_data["href"]}

        return DOM.create_element("a", attribs, props["children"])

    def render_embed(self, props):
        embed_pre_process = app.config.get("EMBED_PRE_PROCESS")
        if embed_pre_process:
            for callback in embed_pre_process:
                callback(props["data"])
        # we use superdesk.etree.parse_html instead of DOM.parse_html as the later modify the content
        # and we use directly the wrapping <div> returned with "content='html'". This works because
        # we use the lxml engine with DraftJSExporter.
        div = parse_html(props["data"]["html"], content="html")
        div.set("class", "embed-block")
        description = props.get("description")
        if description:
            p = DOM.create_element("p", {"class": "embed-block__description"},
                                   description)
            DOM.append_child(div, p)

        return div

    def render_table(self, props):
        num_cols = props["data"]["numCols"]
        num_rows = props["data"]["numRows"]
        with_header = props["data"].get("withHeader", False)
        cells = props["data"]["cells"]
        table = DOM.create_element("table")
        if with_header:
            start_row = 1
            thead = DOM.create_element("thead")
            DOM.append_child(table, thead)
            tr = DOM.create_element("tr")
            DOM.append_child(thead, tr)
            for col_idx in range(num_cols):
                th = DOM.create_element("th")
                DOM.append_child(tr, th)
                try:
                    content_state = cells[0][col_idx]
                except IndexError:
                    continue
                try:
                    content = DOM.parse_html(
                        self.exporter.render(content_state))
                except etree.ParserError:
                    continue
                if content.text or len(content):
                    DOM.append_child(th, content)
        else:
            start_row = 0

        if not with_header or num_rows > 1:
            tbody = DOM.create_element("tbody")
            DOM.append_child(table, tbody)

        for row_idx in range(start_row, num_rows):
            tr = DOM.create_element("tr")
            DOM.append_child(tbody, tr)
            for col_idx in range(num_cols):
                td = DOM.create_element("td")
                DOM.append_child(tr, td)
                try:
                    content_state = cells[row_idx][col_idx]
                except IndexError:
                    continue
                try:
                    content = DOM.parse_html(
                        self.exporter.render(content_state))
                except etree.ParserError:
                    continue
                if content.text or len(content):
                    DOM.append_child(td, content)

        return table

    def style_fallback(self, props):
        type_ = props["inline_style_range"]["style"]
        # we need to use fallback for annotations, has they have suffixes, it's
        # not only "ANNOTATION"
        if type_.startswith("ANNOTATION"):
            attribs = {"annotation-id": type_[11:]}
            return DOM.create_element("span", attribs, props["children"])
        if type_.startswith("COMMENT"):
            # nothing to render for comments
            pass
        else:
            logger.error("No style renderer for {type_!r}".format(type_=type_))
Example #18
0
class DraftJSHTMLExporter:
    def __init__(self, content_editor):
        self.content_editor = content_editor
        # XXX: we need one exporter per DraftJSHTMLExporter because
        #      self.render_media needs to access "entityMap" from local
        #      instance. If MEDIA rendering changes in the future, exporter
        #      should be global for all DraftJSHTMLExporter instances
        self.exporter = HTML({
            'engine':
            DOM.LXML,
            'style_map':
            dict(
                STYLE_MAP, **{
                    INLINE_STYLES.BOLD: 'b',
                    INLINE_STYLES.ITALIC: 'i',
                    INLINE_STYLES.FALLBACK: self.style_fallback,
                }),
            'entity_decorators': {
                ENTITY_TYPES.LINK:
                self.render_link,
                ENTITY_TYPES.HORIZONTAL_RULE:
                lambda props: DOM.create_element('hr'),
                ENTITY_TYPES.EMBED:
                self.render_embed,
                MEDIA:
                self.render_media,
                ANNOTATION:
                self.render_annotation,
                TABLE:
                self.render_table,
            },
        })

    @property
    def content_state(self):
        return self.content_editor.content_state

    def render(self):
        blocks = self.content_state['blocks']
        if not blocks:
            return ''
        if blocks and blocks[0]['text'].strip(
        ) == '' or blocks[-1]['text'].strip() == '':
            # first and last block may be empty due to client constraints, in this case
            # we must discard them during rendering
            content_state = self.content_state.copy()
            content_state['blocks'] = blocks = blocks[:]
            if blocks[0]['text'].strip(
            ) == '' and not blocks[0]['entityRanges']:
                del blocks[0]
            if blocks and blocks[-1]['text'].strip(
            ) == '' and not blocks[-1]['entityRanges']:
                del blocks[-1]
            html = self.exporter.render(content_state)
        else:
            html = self.exporter.render(self.content_state)
        # see render_media for details
        return DUMMY_RE.sub('', html)

    def render_annotation(self, props):
        return props['children']

    def render_media(self, props):
        media_props = props['media']
        media_type = media_props.get('type', 'picture')
        rendition = media_props['renditions'].get(
            'original') or media_props['renditions']['viewImage']
        alt_text = media_props.get('alt_text') or ''
        desc = media_props.get('description_text')
        if media_type == 'picture':
            embed_type = "Image"
            elt = DOM.create_element('img', {
                'src': rendition['href'],
                'alt': alt_text
            }, props['children'])
        elif media_type == 'video':
            embed_type = "Video"
            elt = DOM.create_element(
                'video', {
                    'control': 'control',
                    'src': rendition['href'],
                    'alt': alt_text,
                    'width': '100%',
                    'height': '100%'
                }, props['children'])
        elif media_type == 'audio':
            embed_type = "Audio"
            elt = DOM.create_element(
                'audio', {
                    'control': 'control',
                    'src': rendition['href'],
                    'alt': alt_text,
                    'width': '100%',
                    'height': '100%'
                }, props['children'])
        else:
            logger.error(
                "Invalid or not implemented media type: {media_type}".format(
                    media_type=media_type))
            return None

        content = DOM.render(elt)

        if desc:
            content += "<figcaption>{}</figcaption>".format(desc)

        # we need to retrieve the key, there is not straightforward way to do it
        # so we find the key in entityMap with a corresponding value
        embed_key = next(k for k, v in self.content_state['entityMap'].items()
                         if v['data'].get('media') == props['media'])

        # <dummy_tag> is needed for the comments, because a root node is necessary
        # it will be removed during rendering.
        embed = DOM.parse_html(
            dedent("""\
            <dummy_tag><!-- EMBED START {embed_type} {{id: "editor_{key}"}} -->
            <figure>{content}</figure>
            <!-- EMBED END {embed_type} {{id: "editor_{key}"}} --></dummy_tag>"""
                   ).format(embed_type=embed_type,
                            key=embed_key,
                            content=content))

        return embed

    def render_link(self, props):
        if 'url' in props:
            attribs = {'href': props['url']}
        else:
            link_data = props.get('link', {})
            if link_data.get('attachment'):
                attribs = {'data-attachment': link_data['attachment']}
            elif link_data.get('target'):
                attribs = {
                    'href': link_data['href'],
                    'target': link_data['target']
                }
            else:
                attribs = {'href': link_data['href']}

        return DOM.create_element('a', attribs, props['children'])

    def render_embed(self, props):
        embed_pre_process = app.config.get('EMBED_PRE_PROCESS')
        if embed_pre_process:
            for callback in embed_pre_process:
                callback(props['data'])
        # we use superdesk.etree.parse_html instead of DOM.parse_html as the later modify the content
        # and we use directly the wrapping <div> returned with "content='html'". This works because
        # we use the lxml engine with DraftJSExporter.
        div = parse_html(props['data']['html'], content='html')
        div.set('class', 'embed-block')
        description = props.get('description')
        if description:
            p = DOM.create_element('p', {'class': 'embed-block__description'},
                                   description)
            DOM.append_child(div, p)

        return div

    def render_table(self, props):
        num_cols = props['data']['numCols']
        num_rows = props['data']['numRows']
        with_header = props['data'].get('withHeader', False)
        cells = props['data']['cells']
        table = DOM.create_element('table')
        if with_header:
            start_row = 1
            thead = DOM.create_element('thead')
            DOM.append_child(table, thead)
            tr = DOM.create_element('tr')
            DOM.append_child(thead, tr)
            for col_idx in range(num_cols):
                th = DOM.create_element('th')
                DOM.append_child(tr, th)
                try:
                    content_state = cells[0][col_idx]
                except IndexError:
                    continue
                content = DOM.parse_html(self.exporter.render(content_state))
                if content.text or len(content):
                    DOM.append_child(th, content)
        else:
            start_row = 0

        if not with_header or num_rows > 1:
            tbody = DOM.create_element('tbody')
            DOM.append_child(table, tbody)

        for row_idx in range(start_row, num_rows):
            tr = DOM.create_element('tr')
            DOM.append_child(tbody, tr)
            for col_idx in range(num_cols):
                td = DOM.create_element('td')
                DOM.append_child(tr, td)
                try:
                    content_state = cells[row_idx][col_idx]
                except IndexError:
                    continue
                content = DOM.parse_html(self.exporter.render(content_state))
                if content.text or len(content):
                    DOM.append_child(td, content)

        return table

    def style_fallback(self, props):
        type_ = props['inline_style_range']['style']
        # we need to use fallback for annotations, has they have suffixes, it's
        # not only "ANNOTATION"
        if type_.startswith('ANNOTATION'):
            attribs = {"annotation-id": type_[11:]}
            return DOM.create_element('span', attribs, props['children'])
        else:
            logger.error("No style renderer for {type_!r}".format(type_=type_))
Example #19
0
def memory_consumption_run():
    exporter = HTML(config)

    for content_state in content_states:
        exporter.render(content_state)
Example #20
0
}

exporter = HTML(config)

content_states = get_content_sample()

BENCHMARK_RUNS = int(os.environ.get("BENCHMARK_RUNS", 1))

print(f"Exporting {len(content_states)} ContentStates {BENCHMARK_RUNS} times")

pr = cProfile.Profile()
pr.enable()

for i in range(0, BENCHMARK_RUNS):
    for content_state in content_states:
        exporter.render(content_state)

pr.disable()
p = Stats(pr)

p.strip_dirs().sort_stats("cumulative").print_stats(10)

print("Measuring memory consumption")


@profile(precision=6)
def memory_consumption_run():
    exporter = HTML(config)

    for content_state in content_states:
        exporter.render(content_state)
class TestCompositeDecorator(unittest.TestCase):
    def setUp(self):
        self.exporter = HTML(config)
        self.maxDiff = None

    def test_render_with_entity_and_decorators(self):
        """
        The composite decorator should never render text in any entities.
        """
        self.assertEqual(
            self.exporter.render({
                'entityMap': {
                    '1': {
                        'type': 'LINK',
                        'mutability': 'MUTABLE',
                        'data': {
                            'url': 'http://amazon.us'
                        }
                    }
                },
                'blocks': [
                    {
                        'key': '5s7g9',
                        'text':
                        'search http://a.us or https://yahoo.com or www.google.com for #github and #facebook',
                        'type': 'unstyled',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': [{
                            'offset': 7,
                            'length': 11,
                            'key': 1
                        }],
                    },
                    {
                        'key': '34a12',
                        'text': '#check www.example.com',
                        'type': 'code-block',
                        'inlineStyleRanges': [],
                    },
                ]
            }), '<div>search <a href="http://amazon.us">http://a.us</a> or '
            '<a href="https://yahoo.com">https://yahoo.com</a> or '
            '<a href="http://www.google.com">www.google.com</a> for '
            '<span class="hash_tag">#github</span> and '
            '<span class="hash_tag">#facebook</span></div>'
            '<pre>#check www.example.com</pre>')

    def test_render_with_multiple_decorators(self):
        """
        When multiple decorators match the same part of text,
        only the first one should perform the replacement.
        """
        self.assertEqual(
            self.exporter.render({
                'entityMap': {},
                'blocks': [
                    {
                        'key': '5s7g9',
                        'text':
                        'search http://www.google.com#world for the #world',
                        'type': 'unstyled',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                ]
            }), '<div>search <a href="http://www.google.com#world">'
            'http://www.google.com#world</a> for the '
            '<span class="hash_tag">#world</span></div>')
Example #22
0
def memory_consumption_run():
    exporter = HTML(config)

    for content_state in content_states:
        exporter.render(content_state)
Example #23
0
class TestOutput(unittest.TestCase):
    """
    Test cases related to specific features of the HTML builder.
    """
    def setUp(self):
        self.maxDiff = None
        self.exporter = HTML(config)

    def test_render_empty(self):
        self.assertEqual(self.exporter.render({
            'entityMap': {},
            'blocks': []
        }), '')

    def test_render_emptiest(self):
        self.assertEqual(self.exporter.render({}), '')

    def test_render_with_different_blocks(self):
        self.assertEqual(
            self.exporter.render({
                'entityMap': {},
                'blocks': [{
                    'key': '5s7g9',
                    'text': 'Header',
                    'type': 'header-one',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }, {
                    'key': 'dem5p',
                    'text': 'some paragraph text',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }]
            }), '<h1>Header</h1><p>some paragraph text</p>')

    def test_render_with_unicode(self):
        self.assertEqual(
            self.exporter.render({
                'entityMap': {},
                'blocks': [{
                    'key': 'dem5p',
                    'text': 'Emojis! 🍺',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }]
            }), '<p>Emojis! \U0001f37a</p>')

    def test_render_with_inline_styles(self):
        self.assertEqual(
            self.exporter.render({
                'entityMap': {},
                'blocks': [{
                    'key':
                    'dem5p',
                    'text':
                    'some paragraph text',
                    'type':
                    'unstyled',
                    'depth':
                    0,
                    'inlineStyleRanges': [{
                        'offset': 0,
                        'length': 4,
                        'style': 'ITALIC'
                    }],
                    'entityRanges': []
                }]
            }), '<p><em>some</em> paragraph text</p>')

    def test_render_with_multiple_inline_styles(self):
        self.assertEqual(
            self.exporter.render({
                'entityMap': {
                    '0': {
                        'type': 'LINK',
                        'mutability': 'MUTABLE',
                        'data': {
                            'url': 'http://example.com'
                        }
                    }
                },
                'blocks': [{
                    'key':
                    '5s7g9',
                    'text':
                    'Header',
                    'type':
                    'header-one',
                    'depth':
                    0,
                    'inlineStyleRanges': [{
                        'offset': 0,
                        'length': 2,
                        'style': 'BOLD'
                    }],
                    'entityRanges': []
                }, {
                    'key':
                    'dem5p',
                    'text':
                    'some paragraph text',
                    'type':
                    'unstyled',
                    'depth':
                    0,
                    'inlineStyleRanges': [{
                        'offset': 0,
                        'length': 4,
                        'style': 'HIGHLIGHT'
                    }],
                    'entityRanges': [{
                        'offset': 5,
                        'length': 9,
                        'key': 0
                    }]
                }]
            }),
            '<h1><strong>He</strong>ader</h1><p><strong style="text-decoration: underline;">some</strong> <a href="http://example.com">paragraph</a> text</p>'
        )

    def test_render_with_entities(self):
        self.assertEqual(
            self.exporter.render({
                'entityMap': {
                    '0': {
                        'type': 'LINK',
                        'mutability': 'MUTABLE',
                        'data': {
                            'url': 'http://example.com'
                        }
                    }
                },
                'blocks': [{
                    'key':
                    'dem5p',
                    'text':
                    'some paragraph text',
                    'type':
                    'unstyled',
                    'depth':
                    0,
                    'inlineStyleRanges': [],
                    'entityRanges': [{
                        'offset': 5,
                        'length': 9,
                        'key': 0
                    }]
                }]
            }), '<p>some <a href="http://example.com">paragraph</a> text</p>')

    def test_render_with_entities_crossing_raises(self):
        with self.assertRaises(EntityException):
            self.exporter.render({
                'entityMap': {
                    '0': {
                        'type': 'LINK',
                        'mutability': 'MUTABLE',
                        'data': {
                            'url': 'http://example.com'
                        }
                    },
                    '1': {
                        'type': 'LINK',
                        'mutability': 'MUTABLE',
                        'data': {
                            'url': 'http://bar.example.com'
                        }
                    }
                },
                'blocks': [{
                    'key':
                    'dem5p',
                    'text':
                    'some paragraph text',
                    'type':
                    'unstyled',
                    'depth':
                    0,
                    'inlineStyleRanges': [],
                    'entityRanges': [{
                        'offset': 5,
                        'length': 9,
                        'key': 0
                    }, {
                        'offset': 2,
                        'length': 9,
                        'key': 1
                    }]
                }]
            })

    def test_render_with_wrapping(self):
        self.assertEqual(
            self.exporter.render({
                'entityMap': {},
                'blocks': [{
                    'key': 'dem5p',
                    'text': 'item1',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': [],
                }, {
                    'key': 'dem5p',
                    'text': 'item2',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }]
            }), '<ul class="steps"><li>item1</li><li>item2</li></ul>')

    def test_render_with_number_attribute(self):
        self.assertEqual(
            HTML({
                'block_map':
                dict(
                    BLOCK_MAP, **{
                        BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                            'element': 'li',
                            'wrapper': ['ul', {
                                'length': 5
                            }],
                        },
                    }),
            }).render({
                'entityMap': {},
                'blocks': [
                    {
                        'key': 'dem1p',
                        'text': 'item1',
                        'type': 'unordered-list-item',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': []
                    },
                ],
            }), '<ul length="5"><li>item1</li></ul>')

    def test_render_with_element_options(self):
        self.assertEqual(
            HTML({
                'block_map':
                dict(
                    BLOCK_MAP, **{
                        BLOCK_TYPES.HEADER_TWO: {
                            'element':
                            ['h2', {
                                'className': 'c-amazing-heading'
                            }],
                        },
                    })
            }).render({
                'entityMap': {},
                'blocks': [
                    {
                        'key': 'dem1p',
                        'text': 'item1',
                        'type': 'header-two',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': []
                    },
                ],
            }), '<h2 class="c-amazing-heading">item1</h2>')

    def test_render_with_token_entity(self):
        self.assertEqual(
            self.exporter.render({
                'entityMap': {
                    '2': {
                        'type': 'HORIZONTAL_RULE',
                        'mutability': 'IMMUTABLE',
                        'data': {},
                    },
                },
                'blocks': [
                    {
                        'key': 'dem1p',
                        'text': 'item1',
                        'type': 'unordered-list-item',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': []
                    },
                    {
                        'key': 'dem5p',
                        'text': 'item2',
                        'type': 'unordered-list-item',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': []
                    },
                    {
                        'key': '672oo',
                        'text': ' ',
                        'type': 'atomic',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': [
                            {
                                'offset': 0,
                                'length': 1,
                                'key': 2,
                            },
                        ],
                    },
                ]
            }), '<ul class="steps"><li>item1</li><li>item2</li></ul><hr>')

    def test_render_with_unidirectional_nested_wrapping(self):
        self.assertEqual(
            self.exporter.render({
                'entityMap': {},
                'blocks': [
                    {
                        'key': '93agv',
                        'text': 'A list item',
                        'type': 'unordered-list-item',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': '4ht9m',
                        'text': 'Oops!',
                        'type': 'unordered-list-item',
                        'depth': 1,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'c6gc4',
                        'text': 'Does this support nesting?',
                        'type': 'unordered-list-item',
                        'depth': 2,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'c6gc3',
                        'text': 'Maybe?',
                        'type': 'unordered-list-item',
                        'depth': 2,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': '3mn5b',
                        'text': 'Yep it does!',
                        'type': 'unordered-list-item',
                        'depth': 3,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': '28umf',
                        'text': 'How many levels deep?',
                        'type': 'unordered-list-item',
                        'depth': 4,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'd81ns',
                        'text': 'Lots.',
                        'type': 'unordered-list-item',
                        'depth': 4,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'b0tsc',
                        'text': 'Ah.',
                        'type': 'unordered-list-item',
                        'depth': 4,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                ],
            }),
            '<ul class="steps"><li>A list item<ul class="steps"><li>Oops!<ul class="steps"><li>Does this support nesting?</li><li>Maybe?<ul class="steps"><li>Yep it does!<ul class="steps"><li>How many levels deep?</li><li>Lots.</li><li>Ah.</li></ul></li></ul></li></ul></li></ul></li></ul>'
        )

    def test_render_with_backtracking_nested_wrapping(self):
        self.assertEqual(
            self.exporter.render({
                'entityMap': {},
                'blocks': [
                    {
                        'key': '93agv',
                        'text': 'A list item (0)',
                        'type': 'unordered-list-item',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': '4ht9m',
                        'text': 'Oops! (1)',
                        'type': 'unordered-list-item',
                        'depth': 1,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'c6gc4',
                        'text': 'Does this support nesting? (2)',
                        'type': 'unordered-list-item',
                        'depth': 2,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'c6gc3',
                        'text': 'Maybe? (2)',
                        'type': 'unordered-list-item',
                        'depth': 2,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': '3mn5b',
                        'text': 'Yep it does! (3)',
                        'type': 'unordered-list-item',
                        'depth': 3,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': '28umf',
                        'text': 'How many levels deep? (4)',
                        'type': 'unordered-list-item',
                        'depth': 4,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'c2gc4',
                        'text': 'Backtracking, two at once... (2)',
                        'type': 'unordered-list-item',
                        'depth': 2,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'c1gcb',
                        'text': 'Uh oh (1)',
                        'type': 'unordered-list-item',
                        'depth': 1,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'c2gh4',
                        'text': 'Up, up, and away! (2)',
                        'type': 'unordered-list-item',
                        'depth': 2,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'c1ghb',
                        'text': 'Arh! (1)',
                        'type': 'unordered-list-item',
                        'depth': 1,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'c1gc9',
                        'text': 'Did this work? (0)',
                        'type': 'unordered-list-item',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'c1gc9',
                        'text': 'Yes! (0)',
                        'type': 'unordered-list-item',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                ],
            }),
            '<ul class="steps"><li>A list item (0)<ul class="steps"><li>Oops! (1)<ul class="steps"><li>Does this support nesting? (2)</li><li>Maybe? (2)<ul class="steps"><li>Yep it does! (3)<ul class="steps"><li>How many levels deep? (4)</li></ul></li></ul></li><li>Backtracking, two at once... (2)</li></ul></li><li>Uh oh (1)<ul class="steps"><li>Up, up, and away! (2)</li></ul></li><li>Arh! (1)</li></ul></li><li>Did this work? (0)</li><li>Yes! (0)</li></ul>'
        )

    def test_render_with_jumping_wrapping(self):
        self.assertEqual(
            self.exporter.render({
                'entityMap': {},
                'blocks': [
                    {
                        'key': '93agv',
                        'text': 'A list item (0)',
                        'type': 'unordered-list-item',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': '4ht9m',
                        'text': 'Jumps (2)',
                        'type': 'unordered-list-item',
                        'depth': 2,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'c6gc4',
                        'text': 'Back (0)',
                        'type': 'unordered-list-item',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': 'c6gc3',
                        'text': 'Jumps again (3)',
                        'type': 'unordered-list-item',
                        'depth': 3,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': '3mn5b',
                        'text': 'Back (1)',
                        'type': 'unordered-list-item',
                        'depth': 1,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                ],
            }),
            '<ul class="steps"><li>A list item (0)<ul class="steps"><li><ul class="steps"><li>Jumps (2)</li></ul></li></ul></li><li>Back (0)<ul class="steps"><li><ul class="steps"><li><ul class="steps"><li>Jumps again (3)</li></ul></li></ul></li><li>Back (1)</li></ul></li></ul>'
        )

    def test_render_with_immediate_jumping(self):
        self.assertEqual(
            self.exporter.render({
                'entityMap': {},
                'blocks': [
                    {
                        'key': '93agv',
                        'text': 'A list item (2)',
                        'type': 'unordered-list-item',
                        'depth': 2,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': '93agv',
                        'text': 'A list item (0)',
                        'type': 'unordered-list-item',
                        'depth': 0,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                ],
            }),
            '<ul class="steps"><li><ul class="steps"><li><ul class="steps"><li>A list item (2)</li></ul></li></ul></li><li>A list item (0)</li></ul>'
        )

    def test_render_with_no_zero_depth(self):
        self.assertEqual(
            self.exporter.render({
                'entityMap': {},
                'blocks': [
                    {
                        'key': '93agv',
                        'text': 'A list item (2)',
                        'type': 'unordered-list-item',
                        'depth': 2,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                    {
                        'key': '93agv',
                        'text': 'A list item (2)',
                        'type': 'unordered-list-item',
                        'depth': 2,
                        'inlineStyleRanges': [],
                        'entityRanges': [],
                    },
                ],
            }),
            '<ul class="steps"><li><ul class="steps"><li><ul class="steps"><li>A list item (2)</li><li>A list item (2)</li></ul></li></ul></li></ul>'
        )

    def test_render_with_big_content(self):
        self.assertEqual(
            HTML({
                'entity_decorators': {
                    'LINK': Link()
                },
                'block_map': {
                    'header-two': {
                        'element': 'h2'
                    },
                    'blockquote': {
                        'element': 'blockquote'
                    },
                    'unordered-list-item': {
                        'element': 'li',
                        'wrapper': ['ul', {}]
                    },
                    'unstyled': {
                        'element': 'p'
                    }
                },
                'style_map': {
                    'ITALIC': {
                        'element': 'em'
                    },
                    'BOLD': {
                        'element': 'strong'
                    }
                }
            }).render({
                'entityMap': {
                    '0': {
                        'type': 'LINK',
                        'mutability': 'MUTABLE',
                        'data': {
                            'url': 'http://example.com'
                        }
                    },
                    '1': {
                        'type': 'LINK',
                        'mutability': 'MUTABLE',
                        'data': {
                            'url':
                            'https://www.springload.co.nz/work/nz-festival/'
                        }
                    }
                },
                'blocks': [{
                    'key':
                    '6mgfh',
                    'text':
                    'User experience (UX) design',
                    'type':
                    'header-two',
                    'depth':
                    0,
                    'inlineStyleRanges': [{
                        'offset': 16,
                        'length': 4,
                        'style': 'BOLD'
                    }],
                    'entityRanges': []
                }, {
                    'key': '5384u',
                    'text':
                    'Everyone at Springload applies the best principles of UX to their work.',
                    'type': 'blockquote',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }, {
                    'key': 'eelkd',
                    'text':
                    'The design decisions we make building tools and services for your customers are based on empathy for what your customers need.',
                    'type': 'unstyled',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }, {
                    'key': 'b9grk',
                    'text': 'User research',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }, {
                    'key':
                    'a1tis',
                    'text':
                    'User testing and analysis',
                    'type':
                    'unordered-list-item',
                    'depth':
                    0,
                    'inlineStyleRanges': [],
                    'entityRanges': [{
                        'offset': 0,
                        'length': 25,
                        'key': 0
                    }]
                }, {
                    'key': 'adjdn',
                    'text': 'A/B testing',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }, {
                    'key': '62lio',
                    'text': 'Prototyping',
                    'type': 'unordered-list-item',
                    'depth': 0,
                    'inlineStyleRanges': [],
                    'entityRanges': []
                }, {
                    'key':
                    'fq3f',
                    'text':
                    'How we made it delightful and easy for people to find NZ Festival shows',
                    'type':
                    'unstyled',
                    'depth':
                    0,
                    'inlineStyleRanges': [],
                    'entityRanges': [{
                        'offset': 0,
                        'length': 71,
                        'key': 1
                    }]
                }]
            }),
            '<h2>User experience <strong>(UX)</strong> design</h2><blockquote>Everyone at Springload applies the best principles of UX to their work.</blockquote><p>The design decisions we make building tools and services for your customers are based on empathy for what your customers need.</p><ul><li>User research</li><li><a href="http://example.com">User testing and analysis</a></li><li>A/B testing</li><li>Prototyping</li></ul><p><a href="https://www.springload.co.nz/work/nz-festival/">How we made it delightful and easy for people to find NZ Festival shows</a></p>'
        )

    def test_render_with_default_block_map(self):
        self.assertEqual(
            HTML({
                'style_map': {
                    INLINE_STYLES.ITALIC: {
                        'element': 'em'
                    },
                    INLINE_STYLES.BOLD: {
                        'element': 'strong'
                    },
                    'HIGHLIGHT': {
                        'element': 'strong',
                        'textDecoration': 'underline'
                    },
                },
            }).render({
                'entityMap': {},
                'blocks': [{
                    'key':
                    'dem5p',
                    'text':
                    'some paragraph text',
                    'type':
                    'unstyled',
                    'depth':
                    0,
                    'inlineStyleRanges': [{
                        'offset': 0,
                        'length': 4,
                        'style': 'ITALIC'
                    }],
                    'entityRanges': []
                }]
            }), '<p><em>some</em> paragraph text</p>')

    def test_render_with_default_style_map(self):
        self.assertEqual(
            HTML({
                'block_map':
                dict(
                    BLOCK_MAP, **{
                        BLOCK_TYPES.UNORDERED_LIST_ITEM: {
                            'element': 'li',
                            'wrapper': ['ul', {
                                'className': 'steps'
                            }],
                        },
                        BLOCK_TYPES.ATOMIC: {
                            'element': 'span'
                        },
                    })
            }).render({
                'entityMap': {},
                'blocks': [{
                    'key':
                    'dem5p',
                    'text':
                    'some paragraph text',
                    'type':
                    'unstyled',
                    'depth':
                    0,
                    'inlineStyleRanges': [{
                        'offset': 0,
                        'length': 4,
                        'style': 'ITALIC'
                    }],
                    'entityRanges': []
                }]
            }), '<p><em>some</em> paragraph text</p>')

    def test_render_with_default_config(self):
        self.assertEqual(
            HTML().render({
                'entityMap': {},
                'blocks': [{
                    'key':
                    'dem5p',
                    'text':
                    'some paragraph text',
                    'type':
                    'unstyled',
                    'depth':
                    0,
                    'inlineStyleRanges': [{
                        'offset': 0,
                        'length': 4,
                        'style': 'ITALIC'
                    }],
                    'entityRanges': []
                }]
            }), '<p><em>some</em> paragraph text</p>')