def render_entities(self, style_node): # We have a complete (start, stop) entity to render. if self.completed_entity is not None: entity_details = self.get_entity_details(self.completed_entity) opts = Options.for_entity(self.entity_decorators, entity_details['type']) props = entity_details['data'].copy() props['entity'] = { 'type': entity_details['type'], } if len(self.element_stack) == 1: children = self.element_stack[0] else: children = DOM.create_element() for n in self.element_stack: DOM.append_child(children, n) self.completed_entity = None self.element_stack = [] # Is there still another entity? (adjacent) if so add the current style_node for it. if self.has_entity(): self.element_stack.append(style_node) return DOM.create_element(opts.element, props, children) if self.has_entity(): self.element_stack.append(style_node) return None return style_node
def update_stack(self, options, depth): if depth >= self.stack.length(): # If the depth is gte the stack length, we need more wrappers. depth_levels = range(self.stack.length(), depth + 1) for level in depth_levels: new_wrapper = Wrapper(level, options) # Determine where to append the new wrapper. if self.stack.head().last_child is None: # If there is no content in the current wrapper, we need # to add an intermediary node. props = dict(options.props) props['block'] = { 'type': options.type, 'depth': depth, 'data': {}, } props['blocks'] = self.blocks wrapper_parent = DOM.create_element(options.element, props) DOM.append_child(self.stack.head().elt, wrapper_parent) else: # Otherwise we can append at the end of the last child. wrapper_parent = self.stack.head().last_child DOM.append_child(wrapper_parent, new_wrapper.elt) self.stack.append(new_wrapper) else: # Cut the stack to where it now stops, and add new wrapper. self.stack.slice(depth) self.stack.append(Wrapper(depth, options))
def render_block(self, block, entity_map, wrapper_state): if block['inlineStyleRanges'] or block['entityRanges']: content = DOM.create_element() entity_state = EntityState(self.entity_decorators, entity_map) style_state = StyleState(self.style_map) for (text, commands) in self.build_command_groups(block): for command in commands: entity_state.apply(command) style_state.apply(command) # Decorators are not rendered inside entities. if entity_state.has_no_entity() and self.has_decorators: decorated_node = render_decorators(self.composite_decorators, text, block, wrapper_state.blocks) else: decorated_node = text styled_node = style_state.render_styles(decorated_node, block, wrapper_state.blocks) entity_node = entity_state.render_entities(styled_node) if entity_node is not None: DOM.append_child(content, entity_node) # Check whether there actually are two different nodes, confirming we are not inserting an upcoming entity. if styled_node != entity_node and entity_state.has_no_entity(): DOM.append_child(content, styled_node) # Fast track for blocks which do not contain styles nor entities, which is very common. elif self.has_decorators: content = render_decorators(self.composite_decorators, block['text'], block, wrapper_state.blocks) else: content = block['text'] return wrapper_state.element_for(block, content)
def test_render_www(self): match = next(LINKIFY_DECORATOR['strategy'].finditer('test www.example.com')) self.assertEqual(DOM.render(DOM.create_element(LINKIFY_DECORATOR['component'], { 'block': {'type': BLOCK_TYPES.UNSTYLED}, 'match': match, }, match.group(0))), '<a href="http://www.example.com">www.example.com</a>')
def render_decorators(decorators, text, block, blocks): decorated_children = list(apply_decorators(decorators, text, block, blocks)) if len(decorated_children) == 1: decorated_node = decorated_children[0] else: decorated_node = DOM.create_element() for decorated_child in decorated_children: DOM.append_child(decorated_node, decorated_child) return decorated_node
def __init__(self, config=None): if config is None: config = {} self.entity_decorators = config.get('entity_decorators', {}) self.composite_decorators = config.get('composite_decorators', []) self.has_decorators = len(self.composite_decorators) > 0 self.block_map = config.get('block_map', BLOCK_MAP) self.style_map = config.get('style_map', STYLE_MAP) DOM.use(config.get('engine', DOM.STRING))
def button(props): href = props.get('href', '#') icon_name = props.get('icon', None) text = props.get('text', '') return DOM.create_element( 'a', {'class': 'icon-text' if icon_name else None, 'href': href}, DOM.create_element(icon, {'name': icon_name}) if icon_name else None, DOM.create_element('span', {'class': 'icon-text__text'}, text) if icon_name else text )
def parent_for(self, options, depth, elt): if options.wrapper: parent = self.get_wrapper_elt(options, depth) DOM.append_child(parent, elt) self.stack.stack[-1].last_child = elt else: # Reset the stack if there is no wrapper. self.stack = WrapperStack() parent = elt return parent
def link(props): attributes = {} for key in props: attr = key if key != 'url' else 'href' attributes[attr] = props[key] return DOM.create_element('a', attributes, props['children'])
def test_render_code_block(self): match = next(LINKIFY_DECORATOR['strategy'].finditer('test https://www.example.com')) self.assertEqual(DOM.create_element(LINKIFY_DECORATOR['component'], { 'block': {'type': BLOCK_TYPES.CODE}, 'match': match, }, match.group(0)), match.group(0))
def image(props): return DOM.create_element('img', { 'src': props.get('src'), 'width': props.get('width'), 'height': props.get('height'), 'alt': props.get('alt'), })
def br(props): """ Replace line breaks (\n) with br tags. """ # Do not process matches inside code blocks. if props['block']['type'] == BLOCK_TYPES.CODE: return props['children'] return DOM.create_element('br')
def hashtag(props): """ Wrap hashtags in spans with a specific class. """ # Do not process matches inside code blocks. if props['block']['type'] == BLOCK_TYPES.CODE: return props['children'] return DOM.create_element('span', {'class': 'hashtag'}, props['children'])
def media_embed_entity(props): """ Helper to construct elements of the form <embed embedtype="media" url="https://www.youtube.com/watch?v=y8Kyi0WNg40"/> when converting from contentstate data """ return DOM.create_element('embed', { 'embedtype': 'media', 'url': props.get('url'), })
def __init__(self, depth, options=None): self.depth = depth self.last_child = None if options: self.type = options.wrapper self.props = options.wrapper_props wrapper_props = dict(self.props) if self.props else {} wrapper_props['block'] = { 'type': options.type, 'depth': depth, } self.elt = DOM.create_element(self.type, wrapper_props) else: self.type = None self.props = None self.elt = DOM.create_element()
def document_link_entity(props): """ Helper to construct elements of the form <a id="1" linktype="document">document link</a> when converting from contentstate data """ return DOM.create_element('a', { 'linktype': 'document', 'id': props.get('id'), }, props['children'])
def image_entity(props): """ Helper to construct elements of the form <embed alt="Right-aligned image" embedtype="image" format="right" id="1"/> when converting from contentstate data """ return DOM.create_element('embed', { 'embedtype': 'image', 'format': props.get('format'), 'id': props.get('id'), 'alt': props.get('alt'), })
def render(self, content_state=None): """ Starts the export process on a given piece of content state. """ if content_state is None: content_state = {} blocks = content_state.get('blocks', []) wrapper_state = WrapperState(self.block_map, blocks) document = DOM.create_element() entity_map = content_state.get('entityMap', {}) min_depth = 0 for block in blocks: depth = block['depth'] elt = self.render_block(block, entity_map, wrapper_state) if depth > min_depth: min_depth = depth # At level 0, append the element to the document. if depth == 0: DOM.append_child(document, elt) # If there is no block at depth 0, we need to add the wrapper that contains the whole tree to the document. if min_depth > 0 and wrapper_state.stack.length() != 0: DOM.append_child(document, wrapper_state.stack.tail().elt) return DOM.render(document)
def test_camel_to_dash(self): self.assertEqual(DOM.camel_to_dash('testCamelToDash'), 'test-camel-to-dash') self.assertEqual(DOM.camel_to_dash('TestCamelToDash'), 'test-camel-to-dash') self.assertEqual(DOM.camel_to_dash('TestCamelTODash'), 'test-camel-to-dash') self.assertEqual(DOM.camel_to_dash('TestCamelTODasH'), 'test-camel-to-das-h') self.assertEqual(DOM.camel_to_dash('testcameltodash'), 'testcameltodash') self.assertEqual(DOM.camel_to_dash('test-Camel-ToDash'), 'test-camel-to-dash')
def element_for(self, block, block_content): type_ = block['type'] depth = block['depth'] options = Options.for_block(self.block_map, type_) props = dict(options.props) props['block'] = block props['blocks'] = self.blocks # Make an element from the options specified in the block map. elt = DOM.create_element(options.element, props, block_content) parent = self.parent_for(options, depth, elt) return parent
def link_entity(props): """ <a linktype="page" id="1">internal page link</a> """ id_ = props.get('id') link_props = {} if id_ is not None: link_props['linktype'] = 'page' link_props['id'] = id_ else: link_props['href'] = props.get('url') return DOM.create_element('a', link_props, props['children'])
def render_styles(self, decorated_node, block, blocks): node = decorated_node if not self.is_empty(): # Nest the tags. for style in sorted(self.styles, reverse=True): opt = Options.for_style(self.style_map, style) props = dict(opt.props) props['block'] = block props['blocks'] = blocks props['inline_style_range'] = { 'style': style, } node = DOM.create_element(opt.element, props, node) return node
def block_fallback(props): type_ = props['block']['type'] if type_ == 'example-discard': logging.warn('Missing config for "%s". Discarding block, keeping content.' % type_) # Directly return the block's children to keep its content. return props['children'] elif type_ == 'example-delete': logging.error('Missing config for "%s". Deleting block.' % type_) # Return None to not render anything, removing the whole block. return None else: logging.warn('Missing config for "%s". Using div instead.' % type_) # Provide a fallback. return DOM.create_element('div', {}, props['children'])
def apply_decorators(decorators, text, block, blocks): decorations = get_decorations(decorators, text) pointer = 0 for begin, end, match, decorator in decorations: if pointer < begin: yield text[pointer:begin] yield DOM.create_element(decorator['component'], { 'match': match, 'block': block, 'blocks': blocks, }, match.group(0)) pointer = end if pointer < len(text): yield text[pointer:]
def linkify(props): """ Wrap plain URLs with link tags. """ match = props['match'] protocol = match.group(1) url = match.group(2) href = protocol + url if props['block']['type'] == BLOCK_TYPES.CODE: return href link_props = { 'href': href, } if href.startswith('www'): link_props['href'] = 'http://' + href return DOM.create_element('a', link_props, href)
def hr(props): return DOM.create_element('hr')
def tearDown(self): DOM.use(DOM.HTML5LIB)
def register_core_features(features): # Hallo.js features.register_editor_plugin( 'hallo', 'hr', HalloPlugin( name='hallohr', js=['wagtailadmin/js/hallo-plugins/hallo-hr.js'], order=45, )) features.register_converter_rule( 'editorhtml', 'hr', [WhitelistRule('hr', allow_without_attributes)]) features.register_editor_plugin( 'hallo', 'link', HalloPlugin( name='hallowagtaillink', js=[ 'wagtailadmin/js/page-chooser-modal.js', 'wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js', ], )) features.register_converter_rule('editorhtml', 'link', [ WhitelistRule('a', attribute_rule({'href': check_url})), LinkTypeRule('page', PageLinkHandler), ]) features.register_editor_plugin('hallo', 'bold', HalloFormatPlugin(format_name='bold')) features.register_converter_rule('editorhtml', 'bold', [ WhitelistRule('b', allow_without_attributes), WhitelistRule('strong', allow_without_attributes), ]) features.register_editor_plugin('hallo', 'italic', HalloFormatPlugin(format_name='italic')) features.register_converter_rule('editorhtml', 'italic', [ WhitelistRule('i', allow_without_attributes), WhitelistRule('em', allow_without_attributes), ]) headings_elements = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] headings_order_start = HalloHeadingPlugin.default_order + 1 for order, element in enumerate(headings_elements, start=headings_order_start): features.register_editor_plugin( 'hallo', element, HalloHeadingPlugin(element=element, order=order)) features.register_converter_rule( 'editorhtml', element, [WhitelistRule(element, allow_without_attributes)]) features.register_editor_plugin('hallo', 'ol', HalloListPlugin(list_type='ordered')) features.register_converter_rule('editorhtml', 'ol', [ WhitelistRule('ol', allow_without_attributes), WhitelistRule('li', allow_without_attributes), ]) features.register_editor_plugin('hallo', 'ul', HalloListPlugin(list_type='unordered')) features.register_converter_rule('editorhtml', 'ul', [ WhitelistRule('ul', allow_without_attributes), WhitelistRule('li', allow_without_attributes), ]) # Draftail features.register_editor_plugin( 'draftail', 'hr', draftail_features.BooleanFeature('enableHorizontalRule')) features.register_converter_rule( 'contentstate', 'hr', { 'from_database_format': { 'hr': HorizontalRuleHandler(), }, 'to_database_format': { 'entity_decorators': { 'HORIZONTAL_RULE': lambda props: DOM.create_element('hr') } } }) features.register_editor_plugin( 'draftail', 'h1', draftail_features.BlockFeature({ 'label': 'H1', 'type': 'header-one', 'description': gettext('Heading %(level)d') % { 'level': 1 }, })) features.register_converter_rule( 'contentstate', 'h1', { 'from_database_format': { 'h1': BlockElementHandler('header-one'), }, 'to_database_format': { 'block_map': { 'header-one': 'h1' } } }) features.register_editor_plugin( 'draftail', 'h2', draftail_features.BlockFeature({ 'label': 'H2', 'type': 'header-two', 'description': gettext('Heading %(level)d') % { 'level': 2 }, })) features.register_converter_rule( 'contentstate', 'h2', { 'from_database_format': { 'h2': BlockElementHandler('header-two'), }, 'to_database_format': { 'block_map': { 'header-two': 'h2' } } }) features.register_editor_plugin( 'draftail', 'h3', draftail_features.BlockFeature({ 'label': 'H3', 'type': 'header-three', 'description': gettext('Heading %(level)d') % { 'level': 3 }, })) features.register_converter_rule( 'contentstate', 'h3', { 'from_database_format': { 'h3': BlockElementHandler('header-three'), }, 'to_database_format': { 'block_map': { 'header-three': 'h3' } } }) features.register_editor_plugin( 'draftail', 'h4', draftail_features.BlockFeature({ 'label': 'H4', 'type': 'header-four', 'description': gettext('Heading %(level)d') % { 'level': 4 }, })) features.register_converter_rule( 'contentstate', 'h4', { 'from_database_format': { 'h4': BlockElementHandler('header-four'), }, 'to_database_format': { 'block_map': { 'header-four': 'h4' } } }) features.register_editor_plugin( 'draftail', 'h5', draftail_features.BlockFeature({ 'label': 'H5', 'type': 'header-five', 'description': gettext('Heading %(level)d') % { 'level': 5 }, })) features.register_converter_rule( 'contentstate', 'h5', { 'from_database_format': { 'h5': BlockElementHandler('header-five'), }, 'to_database_format': { 'block_map': { 'header-five': 'h5' } } }) features.register_editor_plugin( 'draftail', 'h6', draftail_features.BlockFeature({ 'label': 'H6', 'type': 'header-six', 'description': gettext('Heading %(level)d') % { 'level': 6 }, })) features.register_converter_rule( 'contentstate', 'h6', { 'from_database_format': { 'h6': BlockElementHandler('header-six'), }, 'to_database_format': { 'block_map': { 'header-six': 'h6' } } }) features.register_editor_plugin( 'draftail', 'ul', draftail_features.BlockFeature({ 'type': 'unordered-list-item', 'icon': 'list-ul', 'description': gettext('Bulleted list'), })) features.register_converter_rule( 'contentstate', 'ul', { 'from_database_format': { 'ul': ListElementHandler('unordered-list-item'), 'li': ListItemElementHandler(), }, 'to_database_format': { 'block_map': { 'unordered-list-item': { 'element': 'li', 'wrapper': 'ul' } } } }) features.register_editor_plugin( 'draftail', 'ol', draftail_features.BlockFeature({ 'type': 'ordered-list-item', 'icon': 'list-ol', 'description': gettext('Numbered list'), })) features.register_converter_rule( 'contentstate', 'ol', { 'from_database_format': { 'ol': ListElementHandler('ordered-list-item'), 'li': ListItemElementHandler(), }, 'to_database_format': { 'block_map': { 'ordered-list-item': { 'element': 'li', 'wrapper': 'ol' } } } }) features.register_editor_plugin( 'draftail', 'blockquote', draftail_features.BlockFeature({ 'type': 'blockquote', 'icon': 'openquote', 'description': gettext('Blockquote'), })) features.register_converter_rule( 'contentstate', 'blockquote', { 'from_database_format': { 'blockquote': BlockElementHandler('blockquote'), }, 'to_database_format': { 'block_map': { 'blockquote': 'blockquote' } } }) features.register_editor_plugin( 'draftail', 'bold', draftail_features.InlineStyleFeature({ 'type': 'BOLD', 'icon': 'bold', 'description': gettext('Bold'), })) features.register_converter_rule( 'contentstate', 'bold', { 'from_database_format': { 'b': InlineStyleElementHandler('BOLD'), 'strong': InlineStyleElementHandler('BOLD'), }, 'to_database_format': { 'style_map': { 'BOLD': 'b' } } }) features.register_editor_plugin( 'draftail', 'italic', draftail_features.InlineStyleFeature({ 'type': 'ITALIC', 'icon': 'italic', 'description': gettext('Italic'), })) features.register_converter_rule( 'contentstate', 'italic', { 'from_database_format': { 'i': InlineStyleElementHandler('ITALIC'), 'em': InlineStyleElementHandler('ITALIC'), }, 'to_database_format': { 'style_map': { 'ITALIC': 'i' } } }) features.register_editor_plugin( 'draftail', 'link', draftail_features.EntityFeature( { 'type': 'LINK', 'icon': 'link', 'description': gettext('Link'), # We want to enforce constraints on which links can be pasted into rich text. # Keep only the attributes Wagtail needs. 'attributes': ['url', 'id', 'parentId'], 'whitelist': { # Keep pasted links with http/https protocol, and not-pasted links (href = undefined). 'href': "^(http:|https:|undefined$)", } }, js=[ 'wagtailadmin/js/page-chooser-modal.js', ])) features.register_converter_rule( 'contentstate', 'link', { 'from_database_format': { 'a[href]': ExternalLinkElementHandler('LINK'), 'a[linktype="page"]': PageLinkElementHandler('LINK'), }, 'to_database_format': { 'entity_decorators': { 'LINK': link_entity } } }) features.register_editor_plugin( 'draftail', 'superscript', draftail_features.InlineStyleFeature({ 'type': 'SUPERSCRIPT', 'icon': 'superscript', 'description': gettext('Superscript'), })) features.register_converter_rule( 'contentstate', 'superscript', { 'from_database_format': { 'sup': InlineStyleElementHandler('SUPERSCRIPT'), }, 'to_database_format': { 'style_map': { 'SUPERSCRIPT': 'sup' } } }) features.register_editor_plugin( 'draftail', 'subscript', draftail_features.InlineStyleFeature({ 'type': 'SUBSCRIPT', 'icon': 'subscript', 'description': gettext('Subscript'), })) features.register_converter_rule( 'contentstate', 'subscript', { 'from_database_format': { 'sub': InlineStyleElementHandler('SUBSCRIPT'), }, 'to_database_format': { 'style_map': { 'SUBSCRIPT': 'sub' } } }) features.register_editor_plugin( 'draftail', 'strikethrough', draftail_features.InlineStyleFeature({ 'type': 'STRIKETHROUGH', 'icon': 'strikethrough', 'description': gettext('Strikethrough'), })) features.register_converter_rule( 'contentstate', 'strikethrough', { 'from_database_format': { 's': InlineStyleElementHandler('STRIKETHROUGH'), }, 'to_database_format': { 'style_map': { 'STRIKETHROUGH': 's' } } }) features.register_editor_plugin( 'draftail', 'code', draftail_features.InlineStyleFeature({ 'type': 'CODE', 'icon': 'code', 'description': gettext('Code'), })) features.register_converter_rule( 'contentstate', 'code', { 'from_database_format': { 'code': InlineStyleElementHandler('CODE'), }, 'to_database_format': { 'style_map': { 'CODE': 'code' } } })
def missing_block(props): return DOM.create_element('div', {'class': 'missing-block'}, props['children'])
def entity_fallback(props): type_ = props['entity']['type'] logging.warn('Missing config for "%s".' % type_) return DOM.create_element('span', {'class': 'missing-entity'}, props['children'])
def list_item(props): depth = props['block']['depth'] return DOM.create_element('li', {'class': 'list-item--depth-{0}'.format(depth)}, props['children'])
def test_image_alt(self): self.assertEqual(DOM.render(image({ 'src': 'test.png', 'alt': 'test', })), '![test](test.png)\n\n')
def test_horizontal_rule(self): self.assertEqual(DOM.render(horizontal_rule({})), '---\n\n')
def test_use_custom(self): DOM.use("tests.test_dom.DOMTestImpl") self.assertEqual(DOM.dom, DOMTestImpl)
def code_block(props): return DOM.create_element('pre', {}, DOM.create_element('code', {}, props['children']))
def test_works(self): self.assertEqual( DOM.render(code_element({ 'block': {}, 'children': 'test', })), 'test\n')
def test_works(self): self.assertEqual( DOM.render(code_wrapper({ 'block': {}, 'children': 'test', })), '```\n')
def test_create_element_empty(self): self.assertEqual( DOM.render_debug(DOM.create_element()), "<fragment></fragment>" )
def test_use_invalid(self): with self.assertRaises(ImportError): DOM.use("test")
def test_use_string(self): DOM.use(DOM.STRING) self.assertEqual(DOM.dom, DOMString)
def test_use_html5lib(self): DOM.use(DOM.HTML5LIB) self.assertEqual(DOM.dom, DOM_HTML5LIB)
def test_append_child(self): parent = DOM.create_element("p") DOM.append_child(parent, DOM.create_element("span", {}, "Test text")) self.assertEqual( DOM.render_debug(parent), "<p><span>Test text</span></p>" )
def test_link(self): self.assertEqual(DOM.render(link({ 'url': 'http://www.example.com/', 'children': 'test', })), '[test](http://www.example.com/)')
def test_parse_html(self): self.assertEqual( DOM.render_debug(DOM.parse_html("<p><span>Test text</span></p>")), "<p><span>Test text</span></p>", )
} } }, # Provide a fallback component (advanced). INLINE_STYLES.FALLBACK: style_fallback, }), '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, # Provide a fallback component (advanced). ENTITY_TYPES.FALLBACK: entity_fallback, }, 'composite_decorators': [ # Use composite decorators to replace text based on a regular expression. { 'strategy': re.compile(r'\n'), 'component': br, }, { 'strategy': re.compile(r'#\w+'),
def test_create_element_entity(self): self.assertEqual( DOM.render_debug(DOM.create_element(icon, {"name": "rocket"})), '<svg class="icon"><use xlink:href="#icon-rocket"></use></svg>', )
def blockquote(props): block_data = props['block']['data'] return DOM.create_element('blockquote', {'cite': block_data.get('cite')}, props['children'])
def ordered_list(props): depth = props['block']['depth'] return DOM.create_element('ol', { 'class': 'list--depth-{0}'.format(depth) }, props['children'])
def ordered_list(props): depth = props['block']['depth'] return DOM.create_element('ol', {'class': 'list--depth-{0}'.format(depth)}, props['children'])
def test_render_without_icon(self): self.assertEqual(DOM.render(DOM.create_element(button, { 'href': 'http://example.com', 'text': 'Launch', })), '<a href="http://example.com">Launch</a>')
def icon(props): href = 'icon-%s' % props.get('name', '') return DOM.create_element('svg', {'class': 'icon'}, DOM.create_element('use', {'xlink:href': href}))
def link(props): return DOM.create_element('a', { 'href': props['url'] }, props['children'])
def missing_inline(props): return DOM.create_element('span', {'class': 'missing-inline'}, props['children'])
def test_render_with_icon(self): self.assertEqual(DOM.render(DOM.create_element(button, { 'href': 'http://example.com', 'icon': 'rocket', 'text': 'Launch', })), '<a class="icon-text" href="http://example.com"><svg class="icon"><use xlink:href="#icon-rocket"></use></svg><span class="icon-text__text">Launch</span></a>')
def link(props): return DOM.create_element('a', {'href': props['url']}, props['children'])
def test_render(self): self.assertEqual(DOM.render(DOM.create_element(link, { 'url': 'http://example.com', }, 'wow')), '<a href="http://example.com">wow</a>')
def br(props): if props['block']['type'] == 'code-block': return props['children'] return DOM.create_element('br')
def test_image(self): self.assertEqual(DOM.render(image({ 'src': 'test.png', })), '![](test.png)\n\n')
def register_core_features(features): # Hallo.js features.register_editor_plugin( 'hallo', 'hr', HalloPlugin( name='hallohr', js=['wagtailadmin/js/hallo-plugins/hallo-hr.js'], order=45, ) ) features.register_converter_rule('editorhtml', 'hr', [ WhitelistRule('hr', allow_without_attributes) ]) features.register_editor_plugin( 'hallo', 'link', HalloPlugin( name='hallowagtaillink', js=['wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js'], ) ) features.register_converter_rule('editorhtml', 'link', [ WhitelistRule('a', attribute_rule({'href': check_url})), LinkTypeRule('page', PageLinkHandler), ]) features.register_editor_plugin( 'hallo', 'bold', HalloFormatPlugin(format_name='bold') ) features.register_converter_rule('editorhtml', 'bold', [ WhitelistRule('b', allow_without_attributes), WhitelistRule('strong', allow_without_attributes), ]) features.register_editor_plugin( 'hallo', 'italic', HalloFormatPlugin(format_name='italic') ) features.register_converter_rule('editorhtml', 'italic', [ WhitelistRule('i', allow_without_attributes), WhitelistRule('em', allow_without_attributes), ]) headings_elements = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] headings_order_start = HalloHeadingPlugin.default_order + 1 for order, element in enumerate(headings_elements, start=headings_order_start): features.register_editor_plugin( 'hallo', element, HalloHeadingPlugin(element=element, order=order) ) features.register_converter_rule('editorhtml', element, [ WhitelistRule(element, allow_without_attributes) ]) features.register_editor_plugin( 'hallo', 'ol', HalloListPlugin(list_type='ordered') ) features.register_converter_rule('editorhtml', 'ol', [ WhitelistRule('ol', allow_without_attributes), WhitelistRule('li', allow_without_attributes), ]) features.register_editor_plugin( 'hallo', 'ul', HalloListPlugin(list_type='unordered') ) features.register_converter_rule('editorhtml', 'ul', [ WhitelistRule('ul', allow_without_attributes), WhitelistRule('li', allow_without_attributes), ]) # Draftail features.register_editor_plugin( 'draftail', 'hr', draftail_features.BooleanFeature('enableHorizontalRule') ) features.register_converter_rule('contentstate', 'hr', { 'from_database_format': { 'hr': HorizontalRuleHandler(), }, 'to_database_format': { 'entity_decorators': {'HORIZONTAL_RULE': lambda props: DOM.create_element('hr')} } }) features.register_editor_plugin( 'draftail', 'h1', draftail_features.BlockFeature({ 'label': 'H1', 'type': 'header-one', 'description': ugettext('Heading {level}').format(level=1), }) ) features.register_converter_rule('contentstate', 'h1', { 'from_database_format': { 'h1': BlockElementHandler('header-one'), }, 'to_database_format': { 'block_map': {'header-one': 'h1'} } }) features.register_editor_plugin( 'draftail', 'h2', draftail_features.BlockFeature({ 'label': 'H2', 'type': 'header-two', 'description': ugettext('Heading {level}').format(level=2), }) ) features.register_converter_rule('contentstate', 'h2', { 'from_database_format': { 'h2': BlockElementHandler('header-two'), }, 'to_database_format': { 'block_map': {'header-two': 'h2'} } }) features.register_editor_plugin( 'draftail', 'h3', draftail_features.BlockFeature({ 'label': 'H3', 'type': 'header-three', 'description': ugettext('Heading {level}').format(level=3), }) ) features.register_converter_rule('contentstate', 'h3', { 'from_database_format': { 'h3': BlockElementHandler('header-three'), }, 'to_database_format': { 'block_map': {'header-three': 'h3'} } }) features.register_editor_plugin( 'draftail', 'h4', draftail_features.BlockFeature({ 'label': 'H4', 'type': 'header-four', 'description': ugettext('Heading {level}').format(level=4), }) ) features.register_converter_rule('contentstate', 'h4', { 'from_database_format': { 'h4': BlockElementHandler('header-four'), }, 'to_database_format': { 'block_map': {'header-four': 'h4'} } }) features.register_editor_plugin( 'draftail', 'h5', draftail_features.BlockFeature({ 'label': 'H5', 'type': 'header-five', 'description': ugettext('Heading {level}').format(level=5), }) ) features.register_converter_rule('contentstate', 'h5', { 'from_database_format': { 'h5': BlockElementHandler('header-five'), }, 'to_database_format': { 'block_map': {'header-five': 'h5'} } }) features.register_editor_plugin( 'draftail', 'h6', draftail_features.BlockFeature({ 'label': 'H6', 'type': 'header-six', 'description': ugettext('Heading {level}').format(level=6), }) ) features.register_converter_rule('contentstate', 'h6', { 'from_database_format': { 'h6': BlockElementHandler('header-six'), }, 'to_database_format': { 'block_map': {'header-six': 'h6'} } }) features.register_editor_plugin( 'draftail', 'ul', draftail_features.BlockFeature({ 'type': 'unordered-list-item', 'icon': 'list-ul', 'description': ugettext('Bulleted list'), }) ) features.register_converter_rule('contentstate', 'ul', { 'from_database_format': { 'ul': ListElementHandler('unordered-list-item'), 'li': ListItemElementHandler(), }, 'to_database_format': { 'block_map': {'unordered-list-item': {'element': 'li', 'wrapper': 'ul'}} } }) features.register_editor_plugin( 'draftail', 'ol', draftail_features.BlockFeature({ 'type': 'ordered-list-item', 'icon': 'list-ol', 'description': ugettext('Numbered list'), }) ) features.register_converter_rule('contentstate', 'ol', { 'from_database_format': { 'ol': ListElementHandler('ordered-list-item'), 'li': ListItemElementHandler(), }, 'to_database_format': { 'block_map': {'ordered-list-item': {'element': 'li', 'wrapper': 'ol'}} } }) features.register_editor_plugin( 'draftail', 'bold', draftail_features.InlineStyleFeature({ 'type': 'BOLD', 'icon': 'bold', 'description': ugettext('Bold'), }) ) features.register_converter_rule('contentstate', 'bold', { 'from_database_format': { 'b': InlineStyleElementHandler('BOLD'), 'strong': InlineStyleElementHandler('BOLD'), }, 'to_database_format': { 'style_map': {'BOLD': 'b'} } }) features.register_editor_plugin( 'draftail', 'italic', draftail_features.InlineStyleFeature({ 'type': 'ITALIC', 'icon': 'italic', 'description': ugettext('Italic'), }) ) features.register_converter_rule('contentstate', 'italic', { 'from_database_format': { 'i': InlineStyleElementHandler('ITALIC'), 'em': InlineStyleElementHandler('ITALIC'), }, 'to_database_format': { 'style_map': {'ITALIC': 'i'} } }) features.register_editor_plugin( 'draftail', 'link', draftail_features.EntityFeature({ 'type': 'LINK', 'icon': 'link', 'description': ugettext('Link'), # We want to enforce constraints on which links can be pasted into rich text. # Keep only the attributes Wagtail needs. 'attributes': ['url', 'id', 'parentId'], 'whitelist': { # Keep pasted links with http/https protocol, and not-pasted links (href = undefined). 'href': "^(http:|https:|undefined$)", } }) ) features.register_converter_rule('contentstate', 'link', { 'from_database_format': { 'a[href]': ExternalLinkElementHandler('LINK'), 'a[linktype="page"]': PageLinkElementHandler('LINK'), }, 'to_database_format': { 'entity_decorators': {'LINK': link_entity} } })
def test_use_lxml(self): DOM.use(DOM.LXML) self.assertEqual(DOM.dom, DOM_LXML)