From 6806cff2d5f8c57c7ca422691c2b4f01d88d4424 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 4 Dec 2017 02:03:05 +0000 Subject: [PATCH] Add skeleton DraftailRichTextArea, with provision for converting from dbHTML to contentstate --- .../rich_text/converters/contentstate.py | 12 + .../converters/contentstate_models.py | 88 +++++++ .../converters/html_to_contentstate.py | 238 ++++++++++++++++++ wagtail/admin/rich_text/editors/draftail.py | 37 +++ 4 files changed, 375 insertions(+) create mode 100644 wagtail/admin/rich_text/converters/contentstate.py create mode 100644 wagtail/admin/rich_text/converters/contentstate_models.py create mode 100644 wagtail/admin/rich_text/converters/html_to_contentstate.py create mode 100644 wagtail/admin/rich_text/editors/draftail.py diff --git a/wagtail/admin/rich_text/converters/contentstate.py b/wagtail/admin/rich_text/converters/contentstate.py new file mode 100644 index 000000000..fb9c040d9 --- /dev/null +++ b/wagtail/admin/rich_text/converters/contentstate.py @@ -0,0 +1,12 @@ +from wagtail.admin.rich_text.converters.html_to_contentstate import HtmlToContentStateHandler + + +class ContentstateConverter(): + def __init__(self, features=None): + self.features = features + self.html_to_contentstate_handler = HtmlToContentStateHandler(features) + + 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=(',', ': ')) diff --git a/wagtail/admin/rich_text/converters/contentstate_models.py b/wagtail/admin/rich_text/converters/contentstate_models.py new file mode 100644 index 000000000..68425819a --- /dev/null +++ b/wagtail/admin/rich_text/converters/contentstate_models.py @@ -0,0 +1,88 @@ +import json +import random +import string + + +class Block(object): + def __init__(self, typ, depth=0): + self.type = typ + self.depth = depth + self.text = "" + self.key = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(5)) + self.inline_style_ranges = [] + self.entity_ranges = [] + + def as_dict(self): + return { + 'key': self.key, + 'type': self.type, + 'depth': self.depth, + 'text': self.text, + 'inlineStyleRanges': [isr.as_dict() for isr in self.inline_style_ranges], + 'entityRanges': [er.as_dict() for er in self.entity_ranges], + } + + +class InlineStyleRange(object): + def __init__(self, style): + self.style = style + self.offset = None + self.length = None + + def as_dict(self): + return { + 'offset': self.offset, + 'length': self.length, + 'style': self.style, + } + + +class Entity(object): + def __init__(self, entity_type, mutability, data): + self.entity_type = entity_type + self.mutability = mutability + self.data = data + + def as_dict(self): + return { + 'mutability': self.mutability, + 'type': self.entity_type, + 'data': self.data, + } + + +class EntityRange(object): + def __init__(self, key): + self.key = key + self.offset = None + self.length = None + + def as_dict(self): + return { + 'key': self.key, + 'offset': self.offset, + 'length': self.length, + } + + +class ContentState(object): + """Pythonic representation of a draft.js contentState structure""" + def __init__(self): + self.blocks = [] + self.entity_count = 0 + self.entity_map = {} + + def add_entity(self, entity): + key = self.entity_count + self.entity_map[key] = entity + self.entity_count += 1 + return key + + def as_dict(self): + return { + 'blocks': [block.as_dict() for block in self.blocks], + 'entityMap': {key: entity.as_dict() for (key, entity) in self.entity_map.items()}, + } + + def as_json(self, **kwargs): + return json.dumps(self.as_dict(), **kwargs) diff --git a/wagtail/admin/rich_text/converters/html_to_contentstate.py b/wagtail/admin/rich_text/converters/html_to_contentstate.py new file mode 100644 index 000000000..a2b9bfee3 --- /dev/null +++ b/wagtail/admin/rich_text/converters/html_to_contentstate.py @@ -0,0 +1,238 @@ +from html.parser import HTMLParser + +from wagtail.admin.rich_text.converters.contentstate_models import ( + Block, ContentState, Entity, EntityRange, InlineStyleRange +) + + +class HandlerState(object): + def __init__(self): + self.current_block = None + self.current_inline_styles = [] + self.current_entity_ranges = [] + self.depth = 0 + self.list_item_type = None + self.pushed_states = [] + + def push(self): + self.pushed_states.append({ + 'current_block': self.current_block, + 'current_inline_styles': self.current_inline_styles, + 'current_entity_ranges': self.current_entity_ranges, + 'depth': self.depth, + 'list_item_type': self.list_item_type + }) + + def pop(self): + last_state = self.pushed_states.pop() + self.current_block = last_state['current_block'] + self.current_inline_styles = last_state['current_inline_styles'] + self.current_entity_ranges = last_state['current_entity_ranges'] + self.depth = last_state['depth'] + self.list_item_type = last_state['list_item_type'] + + +class ListElementHandler(object): + """ Handler for