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 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 image(props): return DOM.create_element('img', { 'src': props.get('src'), 'width': props.get('width'), 'height': props.get('height'), 'alt': props.get('alt'), })
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(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 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 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 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 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 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 __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 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 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 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 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 test_element_for_dismiss_content(self): self.assertEqual( DOM.render_debug( self.wrapper_state.element_for( { 'key': '5s7g9', 'text': 'Paragraph', 'type': 'ignore', 'depth': 0, 'inlineStyleRanges': [], 'entityRanges': [] }, DOM.create_element('img', {'src': '/example.png'}))), '<fragment></fragment>')
def render_link(self, props): if "url" in props: attribs = {"href": props["url"]} else: link_data = props.get("link", {}) if link_data.get("attachment"): attribs = {"data-attachment": link_data["attachment"]} elif link_data.get("target"): attribs = {"href": link_data["href"], "target": link_data["target"]} else: attribs = {"href": link_data["href"]} return DOM.create_element("a", attribs, props["children"])
def test_element_for_element_content(self): self.assertEqual( DOM.render_debug( self.wrapper_state.element_for( { 'key': '5s7g9', 'text': 'Paragraph', 'type': 'unstyled', 'depth': 0, 'inlineStyleRanges': [], 'entityRanges': [] }, DOM.create_element('strong', {}, 'Paragraph'))), '<div><strong>Paragraph</strong></div>')
def test_element_for_no_block(self): self.assertEqual( DOM.render_debug( self.wrapper_state.element_for( { 'key': '5s7g9', 'text': 'Paragraph', 'type': 'atomic', 'depth': 0, 'inlineStyleRanges': [], 'entityRanges': [] }, DOM.create_element('img', {'src': '/example.png'}))), '<img src="/example.png"/>')
def titled_link_entity(props): """ <a linktype="page" id="1">internal page link</a> """ id_ = props.get('id') link_props = {'title': props.get('link_title', '')} 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 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 anchor_entity_decorator(props): """ Draft.js ContentState to database HTML. Converts the ANCHOR entities into <a> tags. """ return DOM.create_element( 'a', { 'data-anchor': True, # 'href': props['fragment'], 'href': '#{}'.format(props['fragment'].lstrip('#')), }, props['children'])
def element_with_uuid(props): added_props = {BLOCK_KEY_NAME: props['block'].get('key')} try: # See if the element is a function - if so, we can only run it and modify its return value to include the data attribute elt = element(props) if elt is not None: elt.attr.update(added_props) return elt except TypeError: # Otherwise we can do the normal process of creating a DOM element with the right element type # and simply adding the data attribute to its props added_props.update(element_props) return DOM.create_element(element, added_props, props['children'])
def test_render_without_icon(self): button = DOM.create_element( Button, {'data': { 'href': 'http://example.com', 'text': 'Launch', }}) self.assertEqual(DOM.get_tag_name(button), 'a') self.assertEqual(DOM.get_text_content(button), 'Launch') self.assertEqual(button.get('href'), 'http://example.com') self.assertEqual(DOM.get_class_list(button), []) self.assertEqual(DOM.render(button), '<a href="http://example.com">Launch</a>')
def mention(props): """ Decorator for the `mention` entity in Draft.js ContentState. This entity is added by the `draft-js-mention-plugin` plugin. (https://www.draft-js-plugins.com/plugin/mention) """ user_id = props.get("mention").get("user") name = props.get("mention").get("name") if name and user_id: return DOM.create_element( "a", { "class": "mention", "href": f"/forum/member/profile/{user_id}/", }, DOM.create_element("span", {"class": "mention"}, f"@{name}"), ) return None
def test_render_with_icon(self): button = DOM.create_element(Button, { 'data': { 'href': 'http://example.com', 'icon': 'rocket', 'text': 'Launch', } }) self.assertEqual(DOM.get_tag_name(button), 'a') self.assertEqual(DOM.get_text_content(button), None) self.assertEqual(button.get('href'), 'http://example.com') self.assertEqual(DOM.get_class_list(button), ['icon-text']) self.assertEqual(DOM.render(button), '<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 news_image_entity_decorator(props): """ Convert our custom image embed from the Draft.js ContentState data into database HTML. """ return DOM.create_element( 'embed', { 'embedtype': 'news-image', 'id': props.get('id'), 'alt': props.get('alt'), 'title': props.get('title'), 'href': props.get('href'), 'width': props.get('width'), })
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 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_entities(self, style_node: Element, block: Block, blocks: Sequence[Block]) -> Element: # 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) options = Options.get(self.entity_options, entity_details['type'], ENTITY_TYPES.FALLBACK) props = entity_details['data'].copy() props['entity'] = { 'type': entity_details['type'], 'mutability': entity_details['mutability'] if 'mutability' in entity_details else None, 'block': block, 'blocks': blocks, 'entity_range': { 'key': self.completed_entity, }, } 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(options.element, props, children) if self.has_entity(): self.element_stack.append(style_node) return None return style_node
def link(props): """ <a linktype="page" id="1">internal page link</a> """ 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'])
def anchor_identifier_entity_decorator(props): """ Draft.js ContentState to database HTML. Converts the ANCHOR entities into <a> tags. """ return DOM.create_element( "a", { "data-id": props["anchor"].lstrip("#"), "id": props["anchor"].lstrip("#"), "href": "#{}".format(props["anchor"].lstrip("#")), }, props["children"], )
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 element_for(self, block, block_content): type_ = block['type'] if 'type' in block else 'unstyled' depth = block['depth'] if 'depth' in block else 0 options = Options.get(self.block_options, type_, BLOCK_TYPES.FALLBACK) 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 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"] = check_url(props.get("url")) return DOM.create_element("a", link_props, props["children"])
def render_table(self, props): num_cols = props["data"]["numCols"] num_rows = props["data"]["numRows"] with_header = props["data"].get("withHeader", False) cells = props["data"]["cells"] table = DOM.create_element("table") if with_header: start_row = 1 thead = DOM.create_element("thead") DOM.append_child(table, thead) tr = DOM.create_element("tr") DOM.append_child(thead, tr) for col_idx in range(num_cols): th = DOM.create_element("th") DOM.append_child(tr, th) try: content_state = cells[0][col_idx] except IndexError: continue try: content = DOM.parse_html( self.exporter.render(content_state)) except etree.ParserError: continue if content.text or len(content): DOM.append_child(th, content) else: start_row = 0 if not with_header or num_rows > 1: tbody = DOM.create_element("tbody") DOM.append_child(table, tbody) for row_idx in range(start_row, num_rows): tr = DOM.create_element("tr") DOM.append_child(tbody, tr) for col_idx in range(num_cols): td = DOM.create_element("td") DOM.append_child(tr, td) try: content_state = cells[row_idx][col_idx] except IndexError: continue try: content = DOM.parse_html( self.exporter.render(content_state)) except etree.ParserError: continue if content.text or len(content): DOM.append_child(td, content) return table
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 image(props): """ Components are simple functions that take `props` as parameter and return DOM elements. This component creates an image element, with the relevant attributes. :param props: :return: """ return DOM.create_element( 'img', { 'src': props.get('src'), 'width': props.get('width'), 'height': props.get('height'), 'alt': props.get('alt'), })
def test_create_element_nested(self): self.assertEqual( DOM.render_debug( DOM.create_element( "a", {}, DOM.create_element( "span", {"class": "file-info icon-text"}, DOM.create_element( "span", {"class": "icon-text__text"}, "Test test" ), DOM.create_element( "svg", {"class": "icon"}, DOM.create_element( "use", {"xlink:href": "#icon-test"} ), ), ), ) ), '<a><span class="file-info icon-text"><span class="icon-text__text">Test test</span><svg class="icon"><use xlink:href="#icon-test"></use></svg></span></a>', )
def replace(self, match, block_type): protocol = match.group(1) url = match.group(2) href = protocol + url if block_type == BLOCK_TYPES.CODE: return DOM.create_text_node(href) text = cgi.escape(href) if href.startswith("www"): href = "http://" + href props = {'href': href} if self.new_window: props.update(target="_blank") return DOM.create_element('a', props, text)
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 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 test_create_element_style_dict(self): self.assertEqual( DOM.render_debug( DOM.create_element( "p", { "style": { "borderColor": "red", "textDecoration": "underline", } }, "Test test", )), '<p style="border-color: red;text-decoration: underline;">Test test</p>', )
def create_node(self, text): text_lines = self.replace_linebreaks(text) if self.is_unstyled(): node = text_lines else: tags = self.get_style_tags() node = DOM.create_element(tags[0]) child = node # Nest the tags. # Set the text and style attribute (if any) on the deepest node. for tag in tags[1:]: new_child = DOM.create_element(tag) DOM.append_child(child, new_child) child = new_child style_value = self.get_style_value() if style_value: DOM.set_attribute(child, 'style', style_value) DOM.append_child(child, text_lines) return node
def set_wrapper(self, options=[], depth=0): if len(options) == 0: element = DOM.create_document_fragment() else: element = DOM.create_element(options[0], options[1]) new_wrapper = [element, depth, options] if depth >= len(self.wrapper_stack): DOM.append_child(DOM.get_children(self.get_wrapper_elt())[-1], element) self.wrapper_stack.append(new_wrapper) else: # Cut the stack to where it now stops, and add new wrapper. self.wrapper_stack = self.wrapper_stack[:depth] + [new_wrapper]
def replace_linebreaks(self, text): lines = text.split('\n') if len(lines) > 1: wrapper = DOM.create_document_fragment() DOM.append_child(wrapper, DOM.create_text_node(lines[0])) for l in lines[1:]: DOM.append_child(wrapper, DOM.create_element('br')) DOM.append_child(wrapper, DOM.create_text_node(l)) else: wrapper = DOM.create_text_node(text) return wrapper
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 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 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 code_block(props): return DOM.create_element('pre', {}, DOM.create_element('code', {}, props['children']))
def br(props): if props['block']['type'] == 'code-block': return props['children'] return DOM.create_element('br')
def blockquote(props): block_data = props['block']['data'] return DOM.create_element('blockquote', { 'cite': block_data.get('cite') }, 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 ordered_list(props): depth = props['block']['depth'] return DOM.create_element('ol', { 'class': 'list--depth-{0}'.format(depth) }, props['children'])
def link(props): return DOM.create_element('a', { 'href': props['url'] }, props['children'])