Project architecture¶
Understanding the high-level architecture will help you navigate the codebase and find where to make changes.
Module organization¶
draftjs_exporter/
html.py # Entry point. HTML class orchestrates the full rendering pipeline.
dom.py # Virtual DOM abstraction. Static facade over interchangeable engines.
command.py # Command model – operations derived from Draft.js ranges.
entity_state.py # Entity (link, image, embed) rendering state machine.
style_state.py # Inline style (bold, italic) nesting state machine.
wrapper_state.py # Block wrapper nesting (e.g. <ul>/<ol> around <li>).
options.py # Configuration normalization – converts user config to internal format.
composite_decorators.py # Regex-based text decorators (line breaks, mentions, linkify).
constants.py # BLOCK_TYPES, ENTITY_TYPES, INLINE_STYLES enums.
defaults.py # Default BLOCK_MAP and STYLE_MAP for HTML.
types.py # Type aliases and TypedDicts for the public API.
error.py # Exception base classes.
engines/
base.py # DOMEngine abstract base class.
string.py # String-concatenation engine (fast, no dependencies, default).
string_compat.py # Backward-compatible variant of the string engine.
html5lib.py # BeautifulSoup / html5lib engine.
lxml.py # lxml engine.
markdown.py # Non-escaping string engine for Markdown output.
markdown/ # Markdown-specific components, config builder, and helpers.
utils/
module_loading.py # import_string() for dotted-path class resolution.
Tests mirror this structure under tests/, with sub-packages for engines/, markdown/, and utils/.
Rendering pipeline¶
The core flow lives in HTML.render() and proceeds as follows:
- Engine setup –
DOM.engine()resolves the engine string to a class (viaimport_string), caches it, and sets it in a thread-safeContextVar. - Option normalization –
Options.map_blocks(),Options.map_styles(),Options.map_entities()convert the user-friendly config maps into flatOptionsMapdicts of type → normalizedOptionsobjects. - Block iteration –
HTML.render()creates aWrapperStateinstance and an empty document fragment, then iterates over each block. - Per-block rendering – For each block,
render_block()extracts text, inline styles, entity ranges, and composite decorators. It builds a sorted list ofCommandobjects from the Draft.js ranges, groups consecutive commands by character offset, and processes each group through: - EntityState – manages an entity stack; onstart_entity/stop_entitypairs it wraps children in entity components. - StyleState – tracks active inline styles;start_segment()keeps style elements open across text segments where styles continue (reducing redundant tags), whilerender_styles()wraps text in nested inline elements from innermost to outermost as a fallback for entity-active segments and callable component styles. - Composite decorators – regex-based text transformations (e.g.\n→<br>). - Wrapper resolution –
wrapper_state.element_for()resolves each block to a DOM element, managing nesting of wrapper elements (e.g.<ul>/<ol>for list items) based on block depth. - Final rendering – All block elements are appended to the document fragment, then
DOM.render()serialises the virtual DOM tree to the output string (HTML or Markdown).
ContentState contract¶
ContentState is often deserialized from a database or an API rather than produced by the editor in front of you, so the exporter draws a line between input it renders around and input it refuses. Integrators need to know which side of that line their content falls on.
What we tolerate¶
A block only has to be a mapping. Every other key has a default, and unknown keys are ignored, so content written by a newer Draft.js does not break an older exporter:
| Key | When it is missing |
|---|---|
type |
Rendered as unstyled |
depth |
Treated as 0 |
text |
Treated as "" |
inlineStyleRanges |
Treated as [] |
entityRanges |
Treated as [] |
key |
Only used for Markdown list lookups |
data |
Only read by components that ask for it |
blocks and entityMap themselves may be missing, and style or entity ranges running past the end of text are clipped rather than rejected. A Markdown list item without a key renders as plain text, since numbering and indentation are looked up by key.
What we refuse¶
- Unmapped values. A block, style, or entity type with no mapping is looked up at
BLOCK_TYPES.FALLBACK,INLINE_STYLES.FALLBACK, orENTITY_TYPES.FALLBACK. When no fallback is configured, rendering raisesConfigException; that is a configuration mistake rather than bad input. See Fallback components. - Dangling references. An entity range pointing at a key that is not in
entityMapraisesExporterEntityException. An unresolvable reference has no sensible rendering, so we report it instead of guessing.
What we do not defend against¶
- Wrongly typed values.
"depth": "1"raisesTypeErrorfrom the depth comparisons, and a block that is not a mapping raisesAttributeError. - Unbounded input. Nesting creates one wrapper element per depth level, and the whole document is rendered at once. Enforce size and depth limits upstream of the exporter.
- Adversarial content. Escaping is the exporter's job, but URL schemes, CSS, and markup emitted by a custom component are the integrator's. See SECURITY.md.
Design goals¶
- Tolerate missing data. Render what is there rather than raising.
- Never silently drop content. A block's text reaches the output unless a configured fallback component explicitly discards it.
- Report only what cannot be rendered. Configuration mistakes and dangling references.
- Do not grow tolerance to match tests. When a property-based test generates input a Draft.js editor could not plausibly produce, narrow the strategy rather than loosening the exporter. See the test strategy.
Engine system¶
The exporter uses a Strategy pattern for output generation. All engines implement the DOMEngine interface (five static methods: create_tag, parse_html, append_child, render, render_debug). The DOM class delegates to the active engine, selected at runtime via strings stored as class constants (DOM.STRING, DOM.HTML5LIB, DOM.LXML, DOM.MARKDOWN, DOM.STRING_COMPAT), short names of the built-in engines ("string"), or dotted import paths for custom engines. Engine selection is thread-safe through a ContextVar.
Engines share the same interface and general behavior, but are not guaranteed to produce byte-identical output for the same input. Each engine delegates serialization to a different underlying library (or none, for string), so real, accepted differences exist.
Key design patterns¶
- Strategy – swappable engines, selected at runtime.
- Facade –
DOMclass hides engine-specific details. - State machine –
EntityStatetracks entity open/close via a stack. - Command pattern – operations on text are modeled as
Commandobjects, sorted, grouped, and applied in order. - Pipeline – text passes through decorators → inline styles → entity wrapping → wrapper nesting, each stage wrapping the previous.
- Null object –
WrapperStack.head()returns a defaultWrapper(-1)when the stack is empty. - Slots for performance – core classes use
__slots__to reduce memory overhead.