From e09a6c80b1ae7f72d6c02c41ec3d4a1415ce29e7 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 15 Feb 2017 17:32:17 +0200 Subject: [PATCH 1/4] Add support for configurable decorators --- draftjs_exporter/composite_decorators.py | 2 +- draftjs_exporter/dom.py | 4 +++- draftjs_exporter/entity_state.py | 2 +- tests/test_composite_decorators.py | 20 ++++++++++++++++++-- tests/test_dom.py | 3 +++ tests/test_entities.py | 16 ++++++++-------- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/draftjs_exporter/composite_decorators.py b/draftjs_exporter/composite_decorators.py index 2927a93..98ebcfb 100644 --- a/draftjs_exporter/composite_decorators.py +++ b/draftjs_exporter/composite_decorators.py @@ -29,7 +29,7 @@ def apply_decorators(decorators, text, block_type): if pointer < begin: yield DOM.create_text_node(text[pointer:begin]) - yield decorator.render({ + yield DOM.create_element(decorator, { 'children': match.group(0), 'match': match, 'block_type': block_type, diff --git a/draftjs_exporter/dom.py b/draftjs_exporter/dom.py index 4ce0fb1..1490e3c 100644 --- a/draftjs_exporter/dom.py +++ b/draftjs_exporter/dom.py @@ -61,9 +61,11 @@ def create_element(type_=None, props=None, *children): if prop is not None: attributes[key] = prop - # "type" is either an entity with a render method, or a tag name. + # "type" is either a class to instantiate, or an object with a render method, or a tag name. if inspect.isclass(type_): elt = type_().render(attributes) + elif callable(getattr(type_, 'render', None)): + elt = type_.render(attributes) else: elt = DOM.create_tag(type_, attributes) diff --git a/draftjs_exporter/entity_state.py b/draftjs_exporter/entity_state.py index 34ee8b4..a91d971 100644 --- a/draftjs_exporter/entity_state.py +++ b/draftjs_exporter/entity_state.py @@ -71,6 +71,6 @@ def render_entitities(self, root_element, style_node): props['children'] = style_node - new_element = decorator.render(props) + new_element = DOM.create_element(decorator, props) DOM.append_child(element_stack[-1], new_element) element_stack.append(new_element) diff --git a/tests/test_composite_decorators.py b/tests/test_composite_decorators.py index d229197..2280620 100644 --- a/tests/test_composite_decorators.py +++ b/tests/test_composite_decorators.py @@ -17,6 +17,9 @@ class Linkify: """ SEARCH_RE = re.compile(r'(http://|https://|www\.)([a-zA-Z0-9\.\-%/\?&_=\+#:~!,\'\*\^$]+)') + def __init__(self, use_new_window=False): + self.use_new_window = use_new_window + def render(self, props): match = props.get('match') protocol = match.group(1) @@ -30,6 +33,10 @@ def render(self, props): 'href': href, } + if self.use_new_window: + link_props['target'] = '_blank' + link_props['rel'] = 'noreferrer noopener' + if href.startswith('www'): link_props['href'] = 'http://' + href @@ -89,11 +96,20 @@ def test_render_www(self): def test_render_code_block(self): match = next(Linkify.SEARCH_RE.finditer('test https://www.example.com')) - self.assertEqual(DOM.render(DOM.create_element(Linkify, { + self.assertEqual(DOM.create_element(Linkify, { 'block_type': BLOCK_TYPES.CODE, 'match': match, 'children': match.group(0), - })), match.group(0)) + }), match.group(0)) + + def test_render_new_window(self): + match = next(Linkify.SEARCH_RE.finditer('test https://www.example.com')) + + self.assertEqual(DOM.render(DOM.create_element(Linkify(use_new_window=True), { + 'block_type': BLOCK_TYPES.UNSTYLED, + 'match': match, + 'children': match.group(0), + })), 'https://www.example.com') class TestHashtag(unittest.TestCase): diff --git a/tests/test_dom.py b/tests/test_dom.py index 6b6d112..d824fff 100644 --- a/tests/test_dom.py +++ b/tests/test_dom.py @@ -29,6 +29,9 @@ def test_create_element_none(self): def test_create_element_entity(self): self.assertEqual(DOM.render(DOM.create_element(Icon, {'name': 'rocket'})), '') + def test_create_element_entity_configured(self): + self.assertEqual(DOM.render(DOM.create_element(Icon(icon_class='i'), {'name': 'rocket'})), '') + def test_create_document_fragment(self): self.assertEqual(DOM.get_tag_name(DOM.create_document_fragment()), 'fragment') diff --git a/tests/test_entities.py b/tests/test_entities.py index e15349a..165c37f 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -39,9 +39,12 @@ def render(self, props): class Icon: + def __init__(self, icon_class='icon'): + self.icon_class = icon_class + def render(self, props): href = 'icon-%s' % props.get('name', '') - return DOM.create_element('svg', {'class': 'icon'}, DOM.create_element('use', {'xlink:href': href})) + return DOM.create_element('svg', {'class': self.icon_class}, DOM.create_element('use', {'xlink:href': href})) class Button: @@ -73,13 +76,10 @@ def test_init(self): self.assertIsInstance(Icon(), Icon) def test_render(self): - icon = DOM.create_element(Icon, { - 'name': 'rocket', - }) - self.assertEqual(DOM.get_tag_name(icon), 'svg') - self.assertEqual(DOM.get_text_content(icon), None) - self.assertEqual(DOM.get_class_list(icon), ['icon']) - self.assertEqual(DOM.render(icon), '') + self.assertEqual(DOM.render(DOM.create_element(Icon, {'name': 'rocket'})), '') + + def test_render_configured(self): + self.assertEqual(DOM.render(DOM.create_element(Icon(icon_class='i'), {'name': 'rocket'})), '') class TestImage(unittest.TestCase): From a113f83c1136754f835f7b1d5a62bae25b31da78 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 15 Feb 2017 17:45:39 +0200 Subject: [PATCH 2/4] Add support for function entities/decorators --- draftjs_exporter/dom.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/draftjs_exporter/dom.py b/draftjs_exporter/dom.py index 1490e3c..788e9c0 100644 --- a/draftjs_exporter/dom.py +++ b/draftjs_exporter/dom.py @@ -61,11 +61,12 @@ def create_element(type_=None, props=None, *children): if prop is not None: attributes[key] = prop - # "type" is either a class to instantiate, or an object with a render method, or a tag name. if inspect.isclass(type_): elt = type_().render(attributes) elif callable(getattr(type_, 'render', None)): elt = type_.render(attributes) + elif callable(type_): + elt = type_(attributes) else: elt = DOM.create_tag(type_, attributes) From a28ef7ad336cea755551ba25ac55dae559f53560 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 15 Feb 2017 19:32:01 +0200 Subject: [PATCH 3/4] Change more examples and tests to use function style entities --- draftjs_exporter/defaults.py | 2 +- example.py | 28 ++++++++++++++++++---------- tests/test_entities.py | 25 ++++--------------------- tests/test_output.py | 11 +++++------ 4 files changed, 28 insertions(+), 38 deletions(-) diff --git a/draftjs_exporter/defaults.py b/draftjs_exporter/defaults.py index e2f9090..2604619 100644 --- a/draftjs_exporter/defaults.py +++ b/draftjs_exporter/defaults.py @@ -17,7 +17,7 @@ # 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.ATOMIC: {'element': 'fragment'}, + BLOCK_TYPES.ATOMIC: {'element': None}, } # Default style map to extend. diff --git a/example.py b/example.py index d733bc5..adbb91f 100644 --- a/example.py +++ b/example.py @@ -11,9 +11,8 @@ from draftjs_exporter.html import HTML -class HR: - def render(self, props): - return DOM.create_element('hr') +def HR(props): + return DOM.create_element('hr') class Image: @@ -29,11 +28,20 @@ def render(self, props): class Link: + def __init__(self, use_new_window=False): + self.use_new_window = use_new_window + def render(self, props): data = props.get('data', {}) - href = data['url'] + link_props = { + 'href': data['url'], + } + + if self.use_new_window: + link_props['target'] = '_blank' + link_props['rel'] = 'noreferrer noopener' - return DOM.create_element('a', {'href': href}, props['children']) + return DOM.create_element('a', link_props, props['children']) class BR: @@ -66,13 +74,13 @@ def render(self, props): config = { 'entity_decorators': { - ENTITY_TYPES.LINK: Link(), - ENTITY_TYPES.IMAGE: Image(), - ENTITY_TYPES.HORIZONTAL_RULE: HR(), + ENTITY_TYPES.LINK: Link(use_new_window=True), + ENTITY_TYPES.IMAGE: Image, + ENTITY_TYPES.HORIZONTAL_RULE: HR, }, 'composite_decorators': [ - BR(), - Hashtag(), + BR, + Hashtag, ], # Extend/override the default block map. 'block_map': dict(BLOCK_MAP, **{ diff --git a/tests/test_entities.py b/tests/test_entities.py index 165c37f..c528bfa 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -5,14 +5,12 @@ from draftjs_exporter.dom import DOM -class Null: - def render(self, props): - return DOM.create_element() +def Null(props): + return DOM.create_element() -class HR: - def render(self, props): - return DOM.create_element('hr') +def HR(props): + return DOM.create_element('hr') class Link: @@ -63,18 +61,12 @@ def render(self, props): class TestNull(unittest.TestCase): - def test_init(self): - self.assertIsInstance(Null(), Null) - def test_render(self): self.assertEqual(DOM.get_tag_name(DOM.create_element(Null, {})), 'fragment') self.assertEqual(DOM.get_text_content(DOM.create_element(Null, {})), None) class TestIcon(unittest.TestCase): - def test_init(self): - self.assertIsInstance(Icon(), Icon) - def test_render(self): self.assertEqual(DOM.render(DOM.create_element(Icon, {'name': 'rocket'})), '') @@ -83,9 +75,6 @@ def test_render_configured(self): class TestImage(unittest.TestCase): - def test_init(self): - self.assertIsInstance(Image(), Image) - def test_render(self): image = DOM.create_element(Image, { 'data': { @@ -100,9 +89,6 @@ def test_render(self): class TestLink(unittest.TestCase): - def test_init(self): - self.assertIsInstance(Link(), Link) - def test_render(self): link = DOM.create_element(Link, { 'data': { @@ -116,9 +102,6 @@ def test_render(self): class TestButton(unittest.TestCase): - def test_init(self): - self.assertIsInstance(Button(), Button) - def test_render_with_icon(self): button = DOM.create_element(Button, { 'data': { diff --git a/tests/test_output.py b/tests/test_output.py index 6ce053d..c4f201c 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -12,13 +12,13 @@ config = { 'entity_decorators': { - ENTITY_TYPES.LINK: Link(), - ENTITY_TYPES.HORIZONTAL_RULE: HR(), + ENTITY_TYPES.LINK: Link, + ENTITY_TYPES.HORIZONTAL_RULE: HR, }, 'composite_decorators': [ - Linkify(), - Hashtag(), - BR(), + Linkify, + Hashtag, + BR, ], 'block_map': dict(BLOCK_MAP, **{ BLOCK_TYPES.UNORDERED_LIST_ITEM: { @@ -849,7 +849,6 @@ def test_render_with_default_style_map(self): 'element': 'li', 'wrapper': ['ul', {'className': 'steps'}], }, - BLOCK_TYPES.ATOMIC: {'element': 'span'}, }) }).render({ 'entityMap': {}, From 7423847a49beddc3cc4e37d196a7b650c27d4e00 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 15 Feb 2017 19:34:22 +0200 Subject: [PATCH 4/4] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 887e4a1..d705f4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html ### Added - Add support for decorators thanks to @su27 (#16, #17). +- Add support for configurable decorators and entities. +- Add support for decorators and entities in function form. ## [[v0.6.2]](https://github.com/springload/draftjs_exporter/releases/tag/v0.6.2) - 2017-01-18