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), })
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 __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, }, })
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 __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 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_unknown_attribute(self): self.assertEqual( HTML({ 'block_map': dict( BLOCK_MAP, **{ BLOCK_TYPES.UNORDERED_LIST_ITEM: { 'element': 'li', 'wrapper': ['ul', { '*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_boolean_attribute_false(self): self.assertEqual( HTML({ 'block_map': dict( BLOCK_MAP, **{ BLOCK_TYPES.UNORDERED_LIST_ITEM: { 'element': 'li', 'wrapper': ['ul', { 'disabled': False }], }, }), }).render({ 'entityMap': {}, 'blocks': [ { 'key': 'dem1p', 'text': 'item1', 'type': 'unordered-list-item', 'depth': 0, 'inlineStyleRanges': [], 'entityRanges': [] }, ] }), '<ul disabled="False"><li>item1</li></ul>')
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_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 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
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))
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))
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__()
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))
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_many_line_breaks(self): self.assertEqual( HTML().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>')
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>')
ENTITY_TYPES.LINK: link, ENTITY_TYPES.DOCUMENT: document, ENTITY_TYPES.HORIZONTAL_RULE: lambda props: DOM.create_element('hr'), ENTITY_TYPES.EMBED: None, ENTITY_TYPES.FALLBACK: entity_fallback, }, 'composite_decorators': [ { 'strategy': re.compile(r'\n'), 'component': br, } ], 'engine': DOM.STRING, } 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()
def memory_consumption_run(): exporter = HTML(config) for content_state in content_states: exporter.render(content_state)
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>')
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>')
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>')
def setUp(self): self.maxDiff = None self.exporter = HTML(config)
class TestOutput(unittest.TestCase): def setUp(self): self.maxDiff = None self.exporter = HTML(config) def test_call_with_different_blocks(self): self.assertEqual(self.exporter.call({ '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_call_with_unicode(self): self.assertEqual(self.exporter.call({ 'entityMap': {}, 'blocks': [ { 'key': 'dem5p', 'text': 'Emojis! 🍺', 'type': 'unstyled', 'depth': 0, 'inlineStyleRanges': [], 'entityRanges': [] } ] }), '<p>Emojis! 🍺</p>') def test_call_with_inline_styles(self): self.assertEqual(self.exporter.call({ '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_call_with_multiple_inline_styles(self): self.assertEqual(self.exporter.call({ '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_call_with_entities(self): self.assertEqual(self.exporter.call({ '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_call_with_entities_crossing_raises(self): with self.assertRaises(EntityException): self.exporter.call({ '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_call_with_wrapping(self): self.assertEqual(self.exporter.call({ '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_call_with_token_entity(self): self.assertEqual(self.exporter.call({ 'entityMap': { '2': { 'type': 'TOKEN', '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': 'horizontal-rule', 'depth': 0, 'inlineStyleRanges': [], 'entityRanges': [ { 'offset': 0, 'length': 1, 'key': 2, }, ], }, ] }), '<ul class="steps"><li>item1</li><li>item2</li></ul><hr>') def test_call_with_unidirectional_nested_wrapping(self): self.assertEqual(self.exporter.call({ '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_call_with_backtracking_nested_wrapping(self): self.assertEqual(self.exporter.call({ '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></ul><ul class="steps"><li>Backtracking, two at once... (2)</li></ul></li></ul><ul class="steps"><li>Uh oh (1)<ul class="steps"><li>Up, up, and away! (2)</li></ul></li></ul><ul class="steps"><li>Arh! (1)</li></ul></li></ul><ul class="steps"><li>Did this work? (0)</li><li>Yes! (0)</li></ul>') def test_call_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'} } }).call({ '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>')
ENTITY_TYPES.IMAGE: image, ENTITY_TYPES.LINK: link, ENTITY_TYPES.DOCUMENT: document, ENTITY_TYPES.HORIZONTAL_RULE: lambda props: DOM.create_element("hr"), ENTITY_TYPES.EMBED: None, ENTITY_TYPES.FALLBACK: entity_fallback, }, "composite_decorators": [{ "strategy": re.compile(r"\n"), "component": br }], "engine": DOM.STRING, } 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()
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 setUp(self): self.exporter = HTML(config)
def test_exports(self): self.maxDiff = None for export in fixtures: exporter = HTML(config) self.assertEqual(exporter.call(export.get('content_state')), export.get('output'))
def test_init_dom_engine_default(self): HTML() self.assertEqual(DOM.dom, DOMString)
'component': br, }, { 'strategy': re.compile(r'#\w+'), 'component': hashtag, }, { 'strategy': LINKIFY_RE, 'component': linkify, }, ], # Specify which DOM backing engine to use. 'engine': DOM.STRING, } exporter = HTML(config) content_state = { "entityMap": { "0": { "type": "LINK", "mutability": "MUTABLE", "data": { "url": "https://github.com/facebook/draft-js" } }, "1": { "type": "LINK", "mutability": "MUTABLE", "data": { "url": "https://facebook.github.io/react/docs/top-level-api.html#react.createelement"
}, { 'strategy': re.compile(r'#\w+'), 'component': hashtag, }, { 'strategy': LINKIFY_RE, 'component': linkify, }, ], # Specify which DOM backing engine to use. 'engine': DOM.STRING, } exporter = HTML(config) content_state = { "entityMap": { "0": { "type": "LINK", "mutability": "MUTABLE", "data": { "url": "https://github.com/facebook/draft-js" } }, "1": { "type": "LINK", "mutability": "MUTABLE", "data": { "url":
BLOCK_TYPES.BLOCKQUOTE: {'element': 'blockquote'}, # TODO Ideally would want double wrapping in pre + code. # See https://github.com/sstur/draft-js-export-html/blob/master/src/stateToHTML.js#L88 BLOCK_TYPES.CODE: {'element': 'pre'}, BLOCK_TYPES.HORIZONTAL_RULE: {'element': 'hr'}, }, 'style_map': { INLINE_STYLES.ITALIC: {'element': 'em'}, INLINE_STYLES.BOLD: {'element': 'strong'}, INLINE_STYLES.CODE: {'element': 'code'}, INLINE_STYLES.STRIKETHROUGH: {'textDecoration': 'line-through'}, INLINE_STYLES.UNDERLINE: {'textDecoration': 'underline'}, }, } exporter = HTML(config) content_state = { '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/',
class TestOutput(unittest.TestCase): def setUp(self): self.maxDiff = None self.exporter = HTML(config) def test_call_with_different_blocks_decodes(self): self.assertEqual(self.exporter.call({ '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_call_with_unicode(self): self.assertEqual(self.exporter.call({ 'entityMap': {}, 'blocks': [ { 'key': 'dem5p', 'text': 'Emojis! 🍺', 'type': 'unstyled', 'depth': 0, 'inlineStyleRanges': [], 'entityRanges': [] } ] }), '<p>Emojis! 🍺</p>') def test_call_with_inline_styles_decodes(self): self.assertEqual(self.exporter.call({ '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_call_with_multiple_inline_styles_decodes(self): self.assertEqual(self.exporter.call({ '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_call_with_entities_decodes(self): self.assertEqual(self.exporter.call({ '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_call_with_entities_crossing_raises(self): with self.assertRaises(EntityException): self.exporter.call({ '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_call_with_wrapped_blocks(self): self.assertEqual(self.exporter.call({ '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_call_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'} } }).call({ '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_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>')
} # Demo content from https://github.com/springload/draftjs_exporter/blob/master/example.py. # with open('docs/example.json') as example: # content_state = json.load(example) if __name__ == '__main__': exporter = HTML({ 'block_map': BLOCK_MAP, 'style_map': STYLE_MAP, 'entity_decorators': { # Map entities to components so they can be rendered with their data. ENTITY_TYPES.IMAGE: image, ENTITY_TYPES.LINK: link, # Lambdas work too. ENTITY_TYPES.HORIZONTAL_RULE: lambda props: DOM.create_element('hr'), # Discard those entities. ENTITY_TYPES.EMBED: None, }, 'engine': 'draftjs_exporter_rust_engine.engine.DOMString', }) markup = exporter.render(content_state) print(markup) # Output to a Markdown file to showcase the output in GitHub (and see changes in git). with codecs.open('docs/example.txt', 'w', 'utf-8') as file:
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'])
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>')