Draft.js exporter
Library to convert rich text from Draft.js raw ContentState to HTML.
It is developed alongside the Draftail rich text editor, for Wagtail. Check out the online demo, and our introductory blog post.
Why
Draft.js is a rich text editor framework for React. Its approach is different from most rich text editors because it does not store data as HTML, but rather in its own representation called ContentState. This exporter is useful when the ContentState to HTML conversion has to be done in a Python ecosystem.
The initial use case was to gain more control over the content managed by rich text editors in a Wagtail/Django site. If you want to read the full story, have a look at our blog post: Rethinking rich text pipelines with Draft.js.
Features
This project adheres to Semantic Versioning, and measures performance and code coverage. Code is checked with mypy.
- Extensive configuration of the generated HTML.
- Default, extensible block & inline style maps for common HTML elements.
- Convert line breaks to
<br>
elements. - Define any attribute in the block map – custom class names for elements.
- React-like API to create custom components.
- Automatic conversion of entity data to HTML attributes (int & boolean to string, style object to style string).
- Nested lists (
<li>
elements go inside <ul>
or <ol>
, with multiple levels). - Output inline styles as inline elements (
<em>
, <strong>
, pick and choose, with any attribute). - Overlapping inline style and entity ranges.
- Static type annotations.
Usage
Draft.js stores data in a JSON representation based on blocks, representing lines of content in the editor, annotated with entities and styles to represent rich text. For more information, this article covers the concepts further.
Getting started
This exporter takes the Draft.js ContentState data as input, and outputs HTML based on its configuration. To get started, install the package:
pip install draftjs_exporter
We support the following Python versions: 3.7, 3.8, 3.9, 3.10, 3.11. For legacy Python versions, find compatible releases in the CHANGELOG.
In your code, create an exporter and use the render
method to create HTML:
from draftjs_exporter.dom import DOM
from draftjs_exporter.html import HTML
config = {}
exporter = HTML(config)
html = exporter.render({
'entityMap': {},
'blocks': [{
'key': '6mgfh',
'text': 'Hello, world!',
'type': 'unstyled',
'depth': 0,
'inlineStyleRanges': [],
'entityRanges': []
}]
})
print(html)
You can also run an example by downloading this repository and then using python example.py
, or by using our online Draft.js demo.
Configuration
The exporter output is extensively configurable to cater for varied rich text requirements.
from draftjs_exporter.constants import BLOCK_TYPES, ENTITY_TYPES
from draftjs_exporter.defaults import BLOCK_MAP, STYLE_MAP
from draftjs_exporter.dom import DOM
config = {
'block_map': dict(BLOCK_MAP, **{
BLOCK_TYPES.HEADER_TWO: 'h2',
BLOCK_TYPES.HEADER_THREE: {'element': 'h3', 'props': {'class': 'u-text-center'}},
BLOCK_TYPES.UNORDERED_LIST_ITEM: {
'element': 'li',
'wrapper': 'ul',
'wrapper_props': {'class': 'bullet-list'},
},
BLOCK_TYPES.BLOCKQUOTE: blockquote,
BLOCK_TYPES.ORDERED_LIST_ITEM: {
'element': list_item,
'wrapper': ordered_list,
},
BLOCK_TYPES.FALLBACK: block_fallback
}),
'style_map': dict(STYLE_MAP, **{
'KBD': 'kbd',
'HIGHLIGHT': {'element': 'strong', 'props': {'style': {'textDecoration': 'underline'}}},
INLINE_STYLES.FALLBACK: style_fallback,
}),
'entity_decorators': {
ENTITY_TYPES.IMAGE: image,
ENTITY_TYPES.LINK: link
ENTITY_TYPES.HORIZONTAL_RULE: lambda props: DOM.create_element('hr'),
ENTITY_TYPES.EMBED: None,
ENTITY_TYPES.FALLBACK: entity_fallback,
},
'composite_decorators': [
{
'strategy': re.compile(r'\n'),
'component': br,
},
{
'strategy': re.compile(r'#\w+'),
'component': hashtag,
},
{
'strategy': LINKIFY_RE,
'component': linkify,
},
],
}
See examples.py for more details.
Advanced usage
Custom components
To generate arbitrary markup with dynamic data, the exporter comes with an API to create rendering components. This API mirrors React’s createElement API (what JSX compiles to).
from draftjs_exporter.dom import DOM
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 blockquote(props):
block_data = props['block']['data']
return DOM.create_element('blockquote', {
'cite': block_data.get('cite')
}, props['children'])
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) if icon_name else text
)
Apart from create_element
, a parse_html
method is also available. Use it to interface with other HTML generators, like template engines.
See examples.py
in the repository for more details.
Fallback components
When dealing with changes in the content schema, as part of ongoing development or migrations, some content can go stale.
To solve this, the exporter allows the definition of fallback components for blocks, styles, and entities.
This feature is only used for development at the moment, if you have a use case for this in production we would love to hear from you.
Please get in touch!
Add the following to the exporter config,
config = {
'block_map': dict(BLOCK_MAP, **{
BLOCK_TYPES.FALLBACK: block_fallback
}),
}
This fallback component can now control the exporter behavior when normal components are not found. Here is an example:
def block_fallback(props):
type_ = props['block']['type']
if type_ == 'example-discard':
logging.warn(f'Missing config for "{type_}". Discarding block, keeping content.')
return props['children']
elif type_ == 'example-delete':
logging.error(f'Missing config for "{type_}". Deleting block.')
return None
else:
logging.warn(f'Missing config for "{type_}". Using div instead.')
return DOM.create_element('div', {}, props['children'])
See examples.py
in the repository for more details.
Alternative backing engines
By default, the exporter uses a dependency-free engine called string
to build the DOM tree. There are alternatives:
html5lib
(via BeautifulSoup)lxml
.string_compat
(A variant of string
with no backwards-incompatible changes since its first release).
The string
engine is the fastest, and does not have any dependencies. Its only drawback is that the parse_html
method does not escape/sanitise HTML like that of other engines.
- For
html5lib
, do pip install draftjs_exporter[html5lib]
. - For
lxml
, do pip install draftjs_exporter[lxml]
. It also requires libxml2
and libxslt
to be available on your system. - There are no additional dependencies for
string_compat
.
Then, use the engine
attribute of the exporter config:
config = {
'engine': DOM.HTML5LIB,
'engine': DOM.LXML,
'engine': DOM.STRING_COMPAT,
}
Custom backing engines
The exporter supports using custom engines to generate its output via the DOM
API. This can be useful to implement custom export formats, e.g. to Markdown (experimental).
Here is an example implementation:
from draftjs_exporter import DOMEngine
class DOMListTree(DOMEngine):
"""
Element tree using nested lists.
"""
@staticmethod
def create_tag(t, attr=None):
return [t, attr, []]
@staticmethod
def append_child(elt, child):
elt[2].append(child)
@staticmethod
def render(elt):
return elt
exporter = HTML({
'engine': 'myproject.example.DOMListTree'
})
Type annotations
The exporter’s codebase uses static type annotations, checked with mypy. Reusable types are made available:
from draftjs_exporter.dom import DOM
from draftjs_exporter.types import Element, Props
def image(props: Props) -> Element:
return DOM.create_element('img', {
'src': props.get('src'),
'width': props.get('width'),
'height': props.get('height'),
'alt': props.get('alt'),
})
Contributing
See anything you like in here? Anything missing? We welcome all support, whether on bug reports, feature requests, code, design, reviews, tests, documentation, and more. Please have a look at our contribution guidelines.
If you just want to set up the project on your own computer, the contribution guidelines also contain all of the setup commands.
Credits
This project is made possible by the work of Springload, a New Zealand digital agency. The beautiful demo site is the work of @thibaudcolas.
View the full list of contributors. MIT licensed.