Draft.js exporter
Python library converting Draft.js raw ContentState JSON into HTML or Markdown. Maintained by Wagtail contributors, developed alongside the Draftail rich text editor.
The public API is small: an HTMLExporter class, a DOM namespace with a React-like create_element, default config maps (BLOCK_MAP, STYLE_MAP) and helpers (code_block, render_children), Markdown helpers (md_*), constants (BLOCK_TYPES, ENTITY_TYPES, INLINE_STYLES), and type aliases (Props, Element, Component, ContentState). Almost everything else is configuration.
Quick reference
You can access a Markdown-native version of every documentation page by adding index.md at the end of the URL.
| Task |
Solution |
Docs |
| Render ContentState to HTML |
HTML({}).render(content_state) |
getting-started |
| Override default blocks / styles |
**BLOCK_MAP, **STYLE_MAP then override keys |
block map |
| Add HTML attributes to a block |
BLOCK_TYPES.X: {"element": "h3", "props": {"class": "…"}} |
block map |
| Wrap adjacent blocks (lists) |
"wrapper": "ul", "wrapper_props": {"class": "…"} |
block map |
| Render entity data (image, link) |
"entity_decorators": {ENTITY_TYPES.LINK: link} |
entity components |
| Read block data / depth in a component |
props["block"]["data"], props["block"]["depth"] |
block components |
| Compose components / pass children |
DOM.create_element(type, props, *children) |
nesting |
| Replace text by regex (line breaks, mentions) |
"composite_decorators": [{"strategy": rx, "component": fn}] |
composite decorators |
| Handle missing block / style / entity types |
BLOCK_TYPES.FALLBACK, INLINE_STYLES.FALLBACK, ENTITY_TYPES.FALLBACK |
fallbacks |
| Discard an entity type |
ENTITY_TYPES.EMBED: None |
entity decorators |
| Switch DOM engine |
"engine": DOM.HTML5LIB (or DOM.LXML, DOM.STRING_COMPAT) |
alternative engines |
| Render Markdown instead of HTML |
HTML(MARKDOWN_CONFIG).render(content_state) |
Markdown |
| Customize Markdown characters |
build_markdown_config({"bold": "__", "italic": "*", ...}) |
Markdown chars |
| Import Markdown as ContentState |
MarkdownImporter({}).import_markdown(md) |
Markdown importer |
| Resolve internal URLs on import (wagtail://) |
scheme_resolver("wagtail", {"page": "LINK"}, coerce={"id": int}) |
Entity resolution |
| Filter imported content (demote headings) |
ContentStateFilter([{"type": "block", "match": ..., "action": "demote"}]) |
Filtering |
| Debug output |
DOM.render_debug(elt), echo '{...}' | python example.py - |
API |
| Migrate between major versions |
DOM.STRING_COMPAT for old string output; per-version notes |
migration guide |
| Full public API (constants, types, defaults) |
draftjs_exporter.constants, types, defaults |
API reference |
Quick start
Install draftjs_exporter from PyPI.
from draftjs_exporter import HTML
exporter = HTML({}) # empty config = use default block/style/entity maps
html = exporter.render(
{
"entityMap": {},
"blocks": [
{
"key": "6m5fh",
"text": "Hello, world!",
"type": "unstyled",
"depth": 0,
"inlineStyleRanges": [],
"entityRanges": [],
}
],
}
)
Debug with real JSON: echo '{"json": "contents"}' | python example.py -. See getting-started.
Configuration
The config is a single dict passed to HTML() with four optional keys plus engine. Each map extends the built-in defaults (BLOCK_MAP, STYLE_MAP) — spread them with ** and override individual keys.
from draftjs_exporter import BLOCK_MAP, BLOCK_TYPES, DOM, ENTITY_TYPES, HTML, STYLE_MAP
import re
config = {
"block_map": {
**BLOCK_MAP,
BLOCK_TYPES.HEADER_TWO: "h2", # string: tag name
BLOCK_TYPES.HEADER_THREE: {
"element": "h3",
"props": {"class": "u-text-center"},
},
BLOCK_TYPES.UNORDERED_LIST_ITEM: { # wrapper for adjacent blocks
"element": "li",
"wrapper": "ul",
"wrapper_props": {"class": "bullet-list"},
},
},
"style_map": {
**STYLE_MAP,
"KBD": "kbd",
"HIGHLIGHT": {
"element": "strong",
"props": {"style": {"textDecoration": "underline"}},
},
},
"entity_decorators": {
ENTITY_TYPES.LINK: lambda props: DOM.create_element(
"a", {"href": props["url"]}, props["children"]
),
ENTITY_TYPES.EMBED: None, # None discards this entity type
},
"composite_decorators": [
{
"strategy": re.compile(r"\n"),
"component": br,
}, # text transformations by regex
],
"engine": DOM.STRING, # default; see Engines below
}
exporter = HTML(config)
See configuration reference for the full shape.
Conventions
- **Extend
BLOCK_MAP and STYLE_MAP with ** spread instead of rebuilding from scratch — they cover the common Draft.js types and styles.
- Use
BLOCK_TYPES / INLINE_STYLES / ENTITY_TYPES constants instead of raw strings, so renames surface as test failures. They also expose FALLBACK.
- Pick a component function only when you need block data, depth, or children composition. A plain string or dict covers most cases.
- Stick with the default
string engine unless you need HTML sanitization (html5lib/lxml).
Custom components
The component API mirrors React's createElement: a function takes a props dict and returns an Element. The props shape differs between entities, blocks, and styles. Reference components from entity_decorators (entities) or block_map / style_map (blocks and styles).
from draftjs_exporter import DOM, Element, Props
# Entity component: receives the entity's `data` dict as props.
def image(props: Props) -> Element:
"""Render an image element from entity data."""
return DOM.create_element(
"img",
{
"src": props.get("src"),
"width": props.get("width"),
"height": props.get("height"),
"alt": props.get("alt"),
},
)
# Block component: receives `block` (Draft.js block object) and `children`.
def blockquote(props: Props) -> Element:
"""Render a blockquote with an optional cite attribute."""
block_data = props["block"]["data"]
return DOM.create_element(
"blockquote", {"cite": block_data.get("cite")}, props["children"]
)
Compose by passing extra positional children to DOM.create_element(type, props, *children). Children can be strings, DOM elements, other components, or None (renders nothing). Pass props["children"] as the last argument so the block's content renders inside the wrapping element. See custom components.
Fallbacks
Each map accepts a FALLBACK key (BLOCK_TYPES.FALLBACK, INLINE_STYLES.FALLBACK, ENTITY_TYPES.FALLBACK) triggered when the exporter hits a type with no explicit mapping. A fallback can return props["children"] (keep content, drop wrapper), None (remove entirely), or any DOM element (alternative rendering). Useful during development and migrations. See fallback components.
Engines
Engines are pluggable serialization strategies selected at runtime via the engine config key. Use the DOM class constants:
| Constant |
Extra install |
Notes |
DOM.STRING |
none (default) |
Fast, dependency-free, no text escaping |
DOM.HTML5LIB |
pip install draftjs_exporter[html5lib] |
Escapes/sanitizes HTML |
DOM.LXML |
pip install draftjs_exporter[lxml] + libxml2/libxslt |
Escapes/sanitizes, alphabetical attrs |
DOM.STRING_COMPAT |
none |
Byte-identical to first release of string |
DOM.MARKDOWN |
none |
Produces Markdown — use MARKDOWN_CONFIG instead |
Engines are not guaranteed to produce byte-identical output. Real differences: attribute ordering (alphabetical for lxml/html5lib, insertion order for string), quote escaping in attributes, attribute-name validation. Expect minor output differences when switching engines — re-check tests that compare rendered HTML exactly. See troubleshooting: exporter behavior.
To build a custom engine, subclass DOMEngine (draftjs_exporter.engines.base) and implement create_tag, append_child, render. Reference it by dotted path: "engine": "my_project.example.DOMListTree". See custom engines.
Markdown
Markdown output is experimental. Prefer MARKDOWN_CONFIG and build_markdown_config over hand-rolling a Markdown config.
from draftjs_exporter import HTML, MARKDOWN_CONFIG
exporter = HTML(MARKDOWN_CONFIG)
markdown = exporter.render(content_state)
Customize characters and fallbacks with build_markdown_config:
from draftjs_exporter import HTML, build_markdown_config
config = build_markdown_config(
{
"bold": "__",
"italic": "*",
"unordered_list_marker": "*",
"ordered_list_delimiter": ")",
"horizontal_rule": "---",
"code_fence": "```",
"style_fallback": None, # None disables fallback (raises instead)
}
)
exporter = HTML(config)
All defaults produce valid CommonMark. The exporter escapes user text so it renders literally rather than as Markdown syntax. Limitations: no underline/subscript/reference-style links/tables; partial bold/italic overlap can produce markers strict parsers reject. See Markdown support.
Importer
The Markdown importer (MarkdownImporter) parses Markdown back into Draft.js ContentState, enabling round-trip workflows (ContentState → Markdown → ContentState). It is dependency-free and covers the CommonMark core. Parsing runs first, then optional filtering applies content policy.
from draftjs_exporter import BLOCK_TYPES, MarkdownImporter, scheme_resolver
importer = MarkdownImporter(
{
"parser_config": {
# Disable constructs, or resolve internal URL schemes to typed entities.
"image_resolvers": [
scheme_resolver(
"wagtail", {"image": "IMAGE"}, coerce={"id": int}, label_key="alt"
),
],
# Whitelist inline HTML tags as styles (no Markdown equivalent).
"inline_html_styles": {"sup": "SUPERSCRIPT", "sub": "SUBSCRIPT"},
},
"filter_rules": [
# Declarative content policy: remove, keep, demote, or a callable.
{"type": "block", "match": BLOCK_TYPES.HEADER_ONE, "action": "demote"},
],
}
)
content_state = importer.import_markdown(markdown)
See Markdown importer. The importer inverts the exporter's text escaping and sized code-span delimiters on round-trip; see Known round-trip limitations for the remaining gaps.
Common gotchas
entity and children are reserved props keys. The exporter overrides them — entity becomes a dict with type/mutability, and children becomes the already-rendered content. Pick entity data keys that avoid them; there is no workaround. See entity props override.
string engine does not escape HTML outside attributes. Use html5lib/lxml if you need escaping/sanitization. DOM.parse_html also provides no sanitization.
- Engine output is not byte-identical across engines. Switching engines produces real differences (attribute order, quote escaping, self-closing tags, attribute-name validation). Update snapshot tests when changing
engine.
- Overlapping inline styles render with minimum tags (e.g.
<strong>Bold <em>Italic</em></strong> rather than reopening <strong>). Semantically equivalent but breaks tests asserting exact strings.
style props accept a dict (camelCase keys) converted to a CSS string. Properties keep insertion order — not sorted alphabetically. Pass style as a string for byte-stable output.
className is not auto-converted to class. Use class directly.
- Engine constants are dotted-path strings, not classes. The exporter imports the class lazily at runtime via
import_string.
unstyled blocks without text render as empty elements (<p></p>), not nothing.
Public API
All imported from draftjs_exporter directly:
HTMLExporter — HTMLExporter(config).render(content_state).
DOM — facade over the active engine. create_element, render, render_debug, parse_html, append_child, camel_to_dash. Engine constants: DOM.STRING, DOM.HTML5LIB, DOM.LXML, DOM.STRING_COMPAT, DOM.MARKDOWN.
- Default maps & configs:
BLOCK_MAP, STYLE_MAP, HTML_CONFIG, MARKDOWN_CONFIG.
- Default components:
code_block (pre > code), render_children (passthrough; used for atomic blocks).
- Constants:
BLOCK_TYPES, INLINE_STYLES, ENTITY_TYPES (each with a FALLBACK member).
- Markdown helper:
build_markdown_config(options), plus the option type alias MarkdownOptions.
- Markdown components:
md_block, md_inline, md_mark_safe, md_link_destination, md_link, md_image, md_prefixed_block, md_make_ul / md_ul, md_make_ol / md_ol, md_list_wrapper, md_code_element / md_code_wrapper, md_inline_style, md_code_span, md_horizontal_rule / md_make_horizontal_rule, md_*_fallback.
- Importer:
MarkdownImporter(config).import_markdown(markdown) — converts Markdown to ContentState. Config keys: parser (dotted path), parser_config (feature toggles, link_resolvers/image_resolvers, inline_html_styles), filter_rules.
- Parser:
MarkdownParser(config).parse(markdown), ParserConfig, scheme_resolver(scheme, type_map, coerce, label_key, mutability), EntityResolver, EntityResolution.
- Filter:
ContentStateFilter(rules).apply(content_state), FilterRule (type/match/action; actions remove/keep/demote/callable).
- Errors:
MarkdownParseError (with .line and .message).
- Type aliases:
Props, Element, Component, ContentState, Block, Entity, EntityMap, EntityRange, InlineStyleRange, RenderableConfig, ExporterConfig, plus internals (CompositeDecorators, ConfigMap, Decorator, EntityKey, Mutability, RenderableType, Tag).
DOMEngine — abstract base for custom engines. Import from draftjs_exporter.engines.base (not re-exported at top level).
For every BLOCK_TYPES.*, INLINE_STYLES.*, ENTITY_TYPES.* value, see the API reference or constants.py.
Resources
1---2name: draftjs-exporter3description: Use when working with the Draft.js exporter library. Manipulating and rendering Draft.js ContentState to HTML or Markdown, parsing Markdown back into ContentState, writing custom block/entity/style components, configuring block/style/entity maps, picking or building DOM, or extending the exporter with fallbacks and composite decorators. Trigger on imports from `draftjs_exporter`, `DOM.create_element`, `block_map` / `style_map` / `entity_decorators` / `composite_decorators`, `build_markdown_config`, `MarkdownImporter` / `ContentStateFilter` / `scheme_resolver`, or Draft.js `ContentState` / `entityMap` JSON.4license: MIT5---67# Draft.js exporter89Python library converting Draft.js raw [ContentState](https://wagtail.github.io/draftjs_exporter/content-state/) JSON into HTML or Markdown. Maintained by [Wagtail](https://wagtail.org/) contributors, developed alongside the [Draftail](https://www.draftail.org/) rich text editor.1011The public API is small: an `HTMLExporter` class, a `DOM` namespace with a React-like `create_element`, default config maps (`BLOCK_MAP`, `STYLE_MAP`) and helpers (`code_block`, `render_children`), Markdown helpers (`md_*`), constants (`BLOCK_TYPES`, `ENTITY_TYPES`, `INLINE_STYLES`), and type aliases (`Props`, `Element`, `Component`, `ContentState`). Almost everything else is configuration.1213## Quick reference1415You can access a Markdown-native version of every documentation page by adding `index.md` at the end of the URL.1617| Task | Solution | Docs |18| --------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |19| Render ContentState to HTML | `HTML({}).render(content_state)` | [getting-started](https://wagtail.github.io/draftjs_exporter/getting-started/) |20| Override default blocks / styles | `**BLOCK_MAP`, `**STYLE_MAP` then override keys | [block map](https://wagtail.github.io/draftjs_exporter/configuration/#block-map) |21| Add HTML attributes to a block | `BLOCK_TYPES.X: {"element": "h3", "props": {"class": "…"}}` | [block map](https://wagtail.github.io/draftjs_exporter/configuration/#block-map) |22| Wrap adjacent blocks (lists) | `"wrapper": "ul", "wrapper_props": {"class": "…"}` | [block map](https://wagtail.github.io/draftjs_exporter/configuration/#block-map) |23| Render entity data (image, link) | `"entity_decorators": {ENTITY_TYPES.LINK: link}` | [entity components](https://wagtail.github.io/draftjs_exporter/custom-components/#entity-components) |24| Read block data / depth in a component | `props["block"]["data"]`, `props["block"]["depth"]` | [block components](https://wagtail.github.io/draftjs_exporter/custom-components/#block-components) |25| Compose components / pass children | `DOM.create_element(type, props, *children)` | [nesting](https://wagtail.github.io/draftjs_exporter/custom-components/#nesting-and-reusing-components) |26| Replace text by regex (line breaks, mentions) | `"composite_decorators": [{"strategy": rx, "component": fn}]` | [composite decorators](https://wagtail.github.io/draftjs_exporter/configuration/#composite-decorators) |27| Handle missing block / style / entity types | `BLOCK_TYPES.FALLBACK`, `INLINE_STYLES.FALLBACK`, `ENTITY_TYPES.FALLBACK` | [fallbacks](https://wagtail.github.io/draftjs_exporter/fallback-components/) |28| Discard an entity type | `ENTITY_TYPES.EMBED: None` | [entity decorators](https://wagtail.github.io/draftjs_exporter/configuration/#entity-decorators) |29| Switch DOM engine | `"engine": DOM.HTML5LIB` (or `DOM.LXML`, `DOM.STRING_COMPAT`) | [alternative engines](https://wagtail.github.io/draftjs_exporter/alternative-engines/) |30| Render Markdown instead of HTML | `HTML(MARKDOWN_CONFIG).render(content_state)` | [Markdown](https://wagtail.github.io/draftjs_exporter/markdown/) |31| Customize Markdown characters | `build_markdown_config({"bold": "__", "italic": "*", ...})` | [Markdown chars](https://wagtail.github.io/draftjs_exporter/markdown/#configuring-output-characters) |32| Import Markdown as ContentState | `MarkdownImporter({}).import_markdown(md)` | [Markdown importer](https://wagtail.github.io/draftjs_exporter/markdown-importer/) |33| Resolve internal URLs on import (wagtail://) | `scheme_resolver("wagtail", {"page": "LINK"}, coerce={"id": int})` | [Entity resolution](https://wagtail.github.io/draftjs_exporter/markdown-importer/#entity-resolution) |34| Filter imported content (demote headings) | `ContentStateFilter([{"type": "block", "match": ..., "action": "demote"}])` | [Filtering](https://wagtail.github.io/draftjs_exporter/markdown-importer/#filtering) |35| Debug output | `DOM.render_debug(elt)`, `echo '{...}' \| python example.py -` | [API](https://wagtail.github.io/draftjs_exporter/api/) |36| Migrate between major versions | `DOM.STRING_COMPAT` for old `string` output; per-version notes | [migration guide](https://wagtail.github.io/draftjs_exporter/migration-guide/) |37| Full public API (constants, types, defaults) | `draftjs_exporter.constants`, `types`, `defaults` | [API reference](https://wagtail.github.io/draftjs_exporter/api/) |3839## Quick start4041Install `draftjs_exporter` from PyPI.4243```python44from draftjs_exporter import HTML4546exporter = HTML({}) # empty config = use default block/style/entity maps4748html = exporter.render(49 {50 "entityMap": {},51 "blocks": [52 {53 "key": "6m5fh",54 "text": "Hello, world!",55 "type": "unstyled",56 "depth": 0,57 "inlineStyleRanges": [],58 "entityRanges": [],59 }60 ],61 }62)63```6465Debug with real JSON: `echo '{"json": "contents"}' | python example.py -`. See [getting-started](https://wagtail.github.io/draftjs_exporter/getting-started/).6667## Configuration6869The config is a single dict passed to `HTML()` with four optional keys plus `engine`. Each map extends the built-in defaults (`BLOCK_MAP`, `STYLE_MAP`) — spread them with `**` and override individual keys.7071```python72from draftjs_exporter import BLOCK_MAP, BLOCK_TYPES, DOM, ENTITY_TYPES, HTML, STYLE_MAP73import re7475config = {76 "block_map": {77 **BLOCK_MAP,78 BLOCK_TYPES.HEADER_TWO: "h2", # string: tag name79 BLOCK_TYPES.HEADER_THREE: {80 "element": "h3",81 "props": {"class": "u-text-center"},82 },83 BLOCK_TYPES.UNORDERED_LIST_ITEM: { # wrapper for adjacent blocks84 "element": "li",85 "wrapper": "ul",86 "wrapper_props": {"class": "bullet-list"},87 },88 },89 "style_map": {90 **STYLE_MAP,91 "KBD": "kbd",92 "HIGHLIGHT": {93 "element": "strong",94 "props": {"style": {"textDecoration": "underline"}},95 },96 },97 "entity_decorators": {98 ENTITY_TYPES.LINK: lambda props: DOM.create_element(99 "a", {"href": props["url"]}, props["children"]100 ),101 ENTITY_TYPES.EMBED: None, # None discards this entity type102 },103 "composite_decorators": [104 {105 "strategy": re.compile(r"\n"),106 "component": br,107 }, # text transformations by regex108 ],109 "engine": DOM.STRING, # default; see Engines below110}111112exporter = HTML(config)113```114115See [configuration reference](https://wagtail.github.io/draftjs_exporter/configuration/) for the full shape.116117### Conventions118119- **Extend `BLOCK_MAP` and `STYLE_MAP` with `**` spread instead of rebuilding from scratch — they cover the common Draft.js types and styles.120- **Use `BLOCK_TYPES` / `INLINE_STYLES` / `ENTITY_TYPES` constants** instead of raw strings, so renames surface as test failures. They also expose `FALLBACK`.121- **Pick a component function only when you need block data, depth, or children composition.** A plain string or dict covers most cases.122- **Stick with the default `string` engine** unless you need HTML sanitization (`html5lib`/`lxml`).123124## Custom components125126The component API mirrors React's `createElement`: a function takes a `props` dict and returns an `Element`. The `props` shape differs between entities, blocks, and styles. Reference components from `entity_decorators` (entities) or `block_map` / `style_map` (blocks and styles).127128```python129from draftjs_exporter import DOM, Element, Props130131132# Entity component: receives the entity's `data` dict as props.133def image(props: Props) -> Element:134 """Render an image element from entity data."""135 return DOM.create_element(136 "img",137 {138 "src": props.get("src"),139 "width": props.get("width"),140 "height": props.get("height"),141 "alt": props.get("alt"),142 },143 )144145146# Block component: receives `block` (Draft.js block object) and `children`.147def blockquote(props: Props) -> Element:148 """Render a blockquote with an optional cite attribute."""149 block_data = props["block"]["data"]150 return DOM.create_element(151 "blockquote", {"cite": block_data.get("cite")}, props["children"]152 )153```154155Compose by passing extra positional children to `DOM.create_element(type, props, *children)`. Children can be strings, DOM elements, other components, or `None` (renders nothing). Pass `props["children"]` as the last argument so the block's content renders inside the wrapping element. See [custom components](https://wagtail.github.io/draftjs_exporter/custom-components/).156157### Fallbacks158159Each map accepts a `FALLBACK` key (`BLOCK_TYPES.FALLBACK`, `INLINE_STYLES.FALLBACK`, `ENTITY_TYPES.FALLBACK`) triggered when the exporter hits a type with no explicit mapping. A fallback can return `props["children"]` (keep content, drop wrapper), `None` (remove entirely), or any DOM element (alternative rendering). Useful during development and migrations. See [fallback components](https://wagtail.github.io/draftjs_exporter/fallback-components/).160161## Engines162163Engines are pluggable serialization strategies selected at runtime via the `engine` config key. Use the `DOM` class constants:164165| Constant | Extra install | Notes |166| ------------------- | ------------------------------------------------------ | ------------------------------------------------- |167| `DOM.STRING` | none (default) | Fast, dependency-free, no text escaping |168| `DOM.HTML5LIB` | `pip install draftjs_exporter[html5lib]` | Escapes/sanitizes HTML |169| `DOM.LXML` | `pip install draftjs_exporter[lxml]` + libxml2/libxslt | Escapes/sanitizes, alphabetical attrs |170| `DOM.STRING_COMPAT` | none | Byte-identical to first release of `string` |171| `DOM.MARKDOWN` | none | Produces Markdown — use `MARKDOWN_CONFIG` instead |172173**Engines are not guaranteed to produce byte-identical output.** Real differences: attribute ordering (alphabetical for `lxml`/`html5lib`, insertion order for `string`), quote escaping in attributes, attribute-name validation. Expect minor output differences when switching engines — re-check tests that compare rendered HTML exactly. See [troubleshooting: exporter behavior](https://wagtail.github.io/draftjs_exporter/troubleshooting/#exporter-behavior).174175To build a custom engine, subclass `DOMEngine` (`draftjs_exporter.engines.base`) and implement `create_tag`, `append_child`, `render`. Reference it by dotted path: `"engine": "my_project.example.DOMListTree"`. See [custom engines](https://wagtail.github.io/draftjs_exporter/custom-engines/).176177## Markdown178179Markdown output is **experimental**. Prefer `MARKDOWN_CONFIG` and `build_markdown_config` over hand-rolling a Markdown config.180181```python182from draftjs_exporter import HTML, MARKDOWN_CONFIG183184exporter = HTML(MARKDOWN_CONFIG)185markdown = exporter.render(content_state)186```187188Customize characters and fallbacks with `build_markdown_config`:189190````python191from draftjs_exporter import HTML, build_markdown_config192193config = build_markdown_config(194 {195 "bold": "__",196 "italic": "*",197 "unordered_list_marker": "*",198 "ordered_list_delimiter": ")",199 "horizontal_rule": "---",200 "code_fence": "```",201 "style_fallback": None, # None disables fallback (raises instead)202 }203)204exporter = HTML(config)205````206207All defaults produce valid [CommonMark](https://commonmark.org/). The exporter [escapes](https://wagtail.github.io/draftjs_exporter/markdown/#escaping) user text so it renders literally rather than as Markdown syntax. Limitations: no underline/subscript/reference-style links/tables; partial bold/italic overlap can produce markers strict parsers reject. See [Markdown support](https://wagtail.github.io/draftjs_exporter/markdown/).208209### Importer210211The Markdown importer (`MarkdownImporter`) parses Markdown back into Draft.js `ContentState`, enabling round-trip workflows (`ContentState → Markdown → ContentState`). It is dependency-free and covers the CommonMark core. Parsing runs first, then optional filtering applies content policy.212213```python214from draftjs_exporter import BLOCK_TYPES, MarkdownImporter, scheme_resolver215216importer = MarkdownImporter(217 {218 "parser_config": {219 # Disable constructs, or resolve internal URL schemes to typed entities.220 "image_resolvers": [221 scheme_resolver(222 "wagtail", {"image": "IMAGE"}, coerce={"id": int}, label_key="alt"223 ),224 ],225 # Whitelist inline HTML tags as styles (no Markdown equivalent).226 "inline_html_styles": {"sup": "SUPERSCRIPT", "sub": "SUBSCRIPT"},227 },228 "filter_rules": [229 # Declarative content policy: remove, keep, demote, or a callable.230 {"type": "block", "match": BLOCK_TYPES.HEADER_ONE, "action": "demote"},231 ],232 }233)234content_state = importer.import_markdown(markdown)235```236237See [Markdown importer](https://wagtail.github.io/draftjs_exporter/markdown-importer/). The importer inverts the exporter's text escaping and sized code-span delimiters on round-trip; see [Known round-trip limitations](https://wagtail.github.io/draftjs_exporter/markdown-importer/#known-round-trip-limitations) for the remaining gaps.238239## Common gotchas2402411. **`entity` and `children` are reserved `props` keys.** The exporter overrides them — `entity` becomes a dict with `type`/`mutability`, and `children` becomes the already-rendered content. Pick entity `data` keys that avoid them; there is no workaround. See [entity props override](https://wagtail.github.io/draftjs_exporter/troubleshooting/#entity-props-override).2422. **`string` engine does not escape HTML outside attributes.** Use `html5lib`/`lxml` if you need escaping/sanitization. `DOM.parse_html` also provides no sanitization.2433. **Engine output is not byte-identical across engines.** Switching engines produces real differences (attribute order, quote escaping, self-closing tags, attribute-name validation). Update snapshot tests when changing `engine`.2444. **Overlapping inline styles render with minimum tags** (e.g. `<strong>Bold <em>Italic</em></strong>` rather than reopening `<strong>`). Semantically equivalent but breaks tests asserting exact strings.2455. **`style` props accept a dict** (camelCase keys) converted to a CSS string. Properties keep insertion order — not sorted alphabetically. Pass `style` as a string for byte-stable output.2466. **`className` is not auto-converted to `class`.** Use `class` directly.2477. **Engine constants are dotted-path strings, not classes.** The exporter imports the class lazily at runtime via `import_string`.2488. **`unstyled` blocks without text render as empty elements** (`<p></p>`), not nothing.249250## Public API251252All imported from `draftjs_exporter` directly:253254- **`HTMLExporter`** — `HTMLExporter(config).render(content_state)`.255- **`DOM`** — facade over the active engine. `create_element`, `render`, `render_debug`, `parse_html`, `append_child`, `camel_to_dash`. Engine constants: `DOM.STRING`, `DOM.HTML5LIB`, `DOM.LXML`, `DOM.STRING_COMPAT`, `DOM.MARKDOWN`.256- **Default maps & configs**: `BLOCK_MAP`, `STYLE_MAP`, `HTML_CONFIG`, `MARKDOWN_CONFIG`.257- **Default components**: `code_block` (`pre` > `code`), `render_children` (passthrough; used for atomic blocks).258- **Constants**: `BLOCK_TYPES`, `INLINE_STYLES`, `ENTITY_TYPES` (each with a `FALLBACK` member).259- **Markdown helper**: `build_markdown_config(options)`, plus the option type alias `MarkdownOptions`.260- **Markdown components**: `md_block`, `md_inline`, `md_mark_safe`, `md_link_destination`, `md_link`, `md_image`, `md_prefixed_block`, `md_make_ul` / `md_ul`, `md_make_ol` / `md_ol`, `md_list_wrapper`, `md_code_element` / `md_code_wrapper`, `md_inline_style`, `md_code_span`, `md_horizontal_rule` / `md_make_horizontal_rule`, `md_*_fallback`.261- **Importer**: `MarkdownImporter(config).import_markdown(markdown)` — converts Markdown to ContentState. Config keys: `parser` (dotted path), `parser_config` (feature toggles, `link_resolvers`/`image_resolvers`, `inline_html_styles`), `filter_rules`.262- **Parser**: `MarkdownParser(config).parse(markdown)`, `ParserConfig`, `scheme_resolver(scheme, type_map, coerce, label_key, mutability)`, `EntityResolver`, `EntityResolution`.263- **Filter**: `ContentStateFilter(rules).apply(content_state)`, `FilterRule` (`type`/`match`/`action`; actions `remove`/`keep`/`demote`/callable).264- **Errors**: `MarkdownParseError` (with `.line` and `.message`).265- **Type aliases**: `Props`, `Element`, `Component`, `ContentState`, `Block`, `Entity`, `EntityMap`, `EntityRange`, `InlineStyleRange`, `RenderableConfig`, `ExporterConfig`, plus internals (`CompositeDecorators`, `ConfigMap`, `Decorator`, `EntityKey`, `Mutability`, `RenderableType`, `Tag`).266- **`DOMEngine`** — abstract base for custom engines. Import from `draftjs_exporter.engines.base` (not re-exported at top level).267268For every `BLOCK_TYPES.*`, `INLINE_STYLES.*`, `ENTITY_TYPES.*` value, see [the API reference](https://wagtail.github.io/draftjs_exporter/api/) or [`constants.py`](https://github.com/wagtail/draftjs_exporter/blob/main/draftjs_exporter/constants.py).269270## Resources271272- [Full docs site](https://wagtail.github.io/draftjs_exporter/)273- [llms-full.txt](https://wagtail.github.io/draftjs_exporter/llms-full.txt)274- [GitHub](https://github.com/wagtail/draftjs_exporter)