diff --git a/setup.py b/setup.py index 87f4e1da8..f25c9cccd 100755 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ install_requires = [ "django-taggit>=0.22.2,<1.0", "django-treebeard>=4.2.0,<5.0", "djangorestframework>=3.1.3,<4.0", + "draftjs_exporter>=1.0,<2.0", "Pillow>=2.6.1,<5.0", "beautifulsoup4>=4.5.1,<5.0", "html5lib>=0.999,<1", diff --git a/wagtail/admin/rich_text/converters/contentstate.py b/wagtail/admin/rich_text/converters/contentstate.py index fb9c040d9..4f3d82094 100644 --- a/wagtail/admin/rich_text/converters/contentstate.py +++ b/wagtail/admin/rich_text/converters/contentstate.py @@ -1,12 +1,172 @@ +import json +import logging +import re + +from draftjs_exporter.constants import BLOCK_TYPES, ENTITY_TYPES, INLINE_STYLES +from draftjs_exporter.defaults import render_children +from draftjs_exporter.dom import DOM +from draftjs_exporter.html import HTML as HTMLExporter + from wagtail.admin.rich_text.converters.html_to_contentstate import HtmlToContentStateHandler +def Image(props): + """ + + """ + return DOM.create_element('embed', { + 'embedtype': 'image', + 'format': props.get('alignment'), + 'id': props.get('id'), + 'alt': props.get('altText'), + }) + + +def Embed(props): + """ + + """ + return DOM.create_element('embed', { + 'embedtype': 'media', + 'url': props.get('url'), + }) + + +def Document(props): + """ + document link + """ + + return DOM.create_element('a', { + 'linktype': 'document', + 'id': props.get('id'), + }, props['children']) + + +def Link(props): + """ + internal page link + """ + link_type = props.get('linkType', '') + link_props = {} + + if link_type == 'page': + link_props['linktype'] = link_type + link_props['id'] = props.get('id') + else: + link_props['href'] = props.get('url') + + return DOM.create_element('a', link_props, props['children']) + + +class BR: + """ + Replace line breaks (\n) with br tags. + """ + SEARCH_RE = re.compile(r'\n') + + def render(self, props): + # Do not process matches inside code blocks. + if props['block']['type'] == BLOCK_TYPES.CODE: + return props['children'] + + return DOM.create_element('br') + + +def BlockFallback(props): + type_ = props['block']['type'] + logging.error('Missing config for "%s". Deleting block.' % type_) + return None + + +def EntityFallback(props): + type_ = props['entity']['type'] + logging.warn('Missing config for "%s". Deleting entity' % type_) + return None + + +EXPORTER_CONFIG_BY_FEATURE = { + 'h1': { + 'block_map': {BLOCK_TYPES.HEADER_ONE: 'h1'} + }, + 'h2': { + 'block_map': {BLOCK_TYPES.HEADER_TWO: 'h2'} + }, + 'h3': { + 'block_map': {BLOCK_TYPES.HEADER_THREE: 'h3'} + }, + 'h4': { + 'block_map': {BLOCK_TYPES.HEADER_FOUR: 'h4'} + }, + 'h5': { + 'block_map': {BLOCK_TYPES.HEADER_FIVE: 'h5'} + }, + 'h6': { + 'block_map': {BLOCK_TYPES.HEADER_SIX: 'h6'} + }, + 'bold': { + 'style_map': {INLINE_STYLES.BOLD: 'b'} + }, + 'italic': { + 'style_map': {INLINE_STYLES.ITALIC: 'i'} + }, + 'ol': { + 'block_map': {BLOCK_TYPES.ORDERED_LIST_ITEM: {'element': 'li', 'wrapper': 'ol'}} + }, + 'ul': { + 'block_map': {BLOCK_TYPES.UNORDERED_LIST_ITEM: {'element': 'li', 'wrapper': 'ul'}} + }, + 'hr': { + 'entity_decorators': {ENTITY_TYPES.HORIZONTAL_RULE: lambda props: DOM.create_element('hr')} + }, + 'link': { + 'entity_decorators': {ENTITY_TYPES.LINK: Link} + }, + 'document-link': { + 'entity_decorators': {ENTITY_TYPES.DOCUMENT: Document} + }, + 'image': { + 'entity_decorators': {ENTITY_TYPES.IMAGE: Image} + }, + 'embed': { + 'entity_decorators': {ENTITY_TYPES.EMBED: Embed} + }, +} + + class ContentstateConverter(): def __init__(self, features=None): self.features = features self.html_to_contentstate_handler = HtmlToContentStateHandler(features) + exporter_config = { + 'block_map': { + BLOCK_TYPES.UNSTYLED: 'p', + BLOCK_TYPES.ATOMIC: render_children, + BLOCK_TYPES.FALLBACK: BlockFallback, + }, + 'style_map': {}, + 'entity_decorators': { + ENTITY_TYPES.FALLBACK: EntityFallback, + }, + 'composite_decorators': [ + BR, + ], + 'engine': 'html5lib', + } + + for feature in self.features: + feature_config = EXPORTER_CONFIG_BY_FEATURE.get(feature, {}) + 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) 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))