From 75bd2b5a14c61ab9d7761200d0b860a1f93458b1 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Fri, 16 Feb 2018 10:30:06 +0000 Subject: [PATCH] Gracefully handle inline styles/entities at top-level of rich text (#4290) --- .../converters/html_to_contentstate.py | 16 +++++- wagtail/admin/tests/test_contentstate.py | 56 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/wagtail/admin/rich_text/converters/html_to_contentstate.py b/wagtail/admin/rich_text/converters/html_to_contentstate.py index 9d5ce3d28..ea1c07a5f 100644 --- a/wagtail/admin/rich_text/converters/html_to_contentstate.py +++ b/wagtail/admin/rich_text/converters/html_to_contentstate.py @@ -103,7 +103,13 @@ class InlineStyleElementHandler: self.style = style def handle_starttag(self, name, attrs, state, contentstate): - assert state.current_block is not None, "%s element found at the top level" % name + if state.current_block is None: + # Inline style element encountered at the top level - + # start a new paragraph block to contain it + block = Block('unstyled', depth=state.list_depth) + contentstate.blocks.append(block) + state.current_block = block + state.leading_whitespace = STRIP_WHITESPACE if state.leading_whitespace == FORCE_WHITESPACE: # any pending whitespace should be output before handling this tag, @@ -131,7 +137,13 @@ class InlineEntityElementHandler: self.entity_type = entity_type def handle_starttag(self, name, attrs, state, contentstate): - assert state.current_block is not None, "%s element found at the top level" % name + if state.current_block is None: + # Inline entity element encountered at the top level - + # start a new paragraph block to contain it + block = Block('unstyled', depth=state.list_depth) + contentstate.blocks.append(block) + state.current_block = block + state.leading_whitespace = STRIP_WHITESPACE if state.leading_whitespace == FORCE_WHITESPACE: # any pending whitespace should be output before handling this tag, diff --git a/wagtail/admin/tests/test_contentstate.py b/wagtail/admin/tests/test_contentstate.py index 8c3c9f6dd..3c277fa5d 100644 --- a/wagtail/admin/tests/test_contentstate.py +++ b/wagtail/admin/tests/test_contentstate.py @@ -144,6 +144,24 @@ class TestHtmlToContentState(TestCase): ] }) + def test_inline_styles_at_start_of_bare_block(self): + converter = ContentstateConverter(features=['bold', 'italic']) + result = json.loads(converter.from_database_format( + '''Seriously, stop talking about Fight Club already.''' + )) + self.assertContentStateEqual(result, { + 'entityMap': {}, + 'blocks': [ + { + 'inlineStyleRanges': [ + {'offset': 0, 'length': 9, 'style': 'BOLD'}, + {'offset': 30, 'length': 10, 'style': 'ITALIC'}, + ], + 'text': 'Seriously, stop talking about Fight Club already.', 'depth': 0, 'type': 'unstyled', 'key': '00000', 'entityRanges': [] + }, + ] + }) + def test_inline_styles_depend_on_features(self): converter = ContentstateConverter(features=['italic', 'just-made-it-up']) result = json.loads(converter.from_database_format( @@ -237,6 +255,44 @@ class TestHtmlToContentState(TestCase): ] }) + def test_link_in_bare_text(self): + converter = ContentstateConverter(features=['link']) + result = json.loads(converter.from_database_format( + '''an external link''' + )) + self.assertContentStateEqual(result, { + 'entityMap': { + '0': {'mutability': 'MUTABLE', 'type': 'LINK', 'data': {'url': 'http://wagtail.io'}} + }, + 'blocks': [ + { + 'inlineStyleRanges': [], 'text': 'an external link', 'depth': 0, 'type': 'unstyled', 'key': '00000', + 'entityRanges': [{'offset': 3, 'length': 8, 'key': 0}] + }, + ] + }) + + def test_link_at_start_of_bare_text(self): + converter = ContentstateConverter(features=['link']) + result = json.loads(converter.from_database_format( + '''an external link and another''' + )) + self.assertContentStateEqual(result, { + 'entityMap': { + '0': {'mutability': 'MUTABLE', 'type': 'LINK', 'data': {'url': 'http://wagtail.io'}}, + '1': {'mutability': 'MUTABLE', 'type': 'LINK', 'data': {'url': 'http://torchbox.com'}}, + }, + 'blocks': [ + { + 'inlineStyleRanges': [], 'text': 'an external link and another', 'depth': 0, 'type': 'unstyled', 'key': '00000', + 'entityRanges': [ + {'offset': 0, 'length': 16, 'key': 0}, + {'offset': 21, 'length': 7, 'key': 1}, + ] + }, + ] + }) + def test_page_link(self): converter = ContentstateConverter(features=['link']) result = json.loads(converter.from_database_format(