Skip to main content

Library to convert the Facebook Draft.js editor's raw ContentState to HTML

Project description

draftjs_exporter [![Build Status](https://travis-ci.org/springload/draftjs_exporter.svg?branch=master)](https://travis-ci.org/springload/draftjs_exporter) [![Code Climate](https://codeclimate.com/github/springload/draftjs_exporter/badges/gpa.svg)](https://codeclimate.com/github/springload/draftjs_exporter) [![Coverage Status](https://coveralls.io/repos/github/springload/draftjs_exporter/badge.svg?branch=master)](https://coveralls.io/github/springload/draftjs_exporter?branch=master)
================

> Library to convert the Facebook Draft.js editor's raw ContentState to HTML. Python port of the [draftjs_exporter](https://github.com/ignitionworks/draftjs_exporter) ruby library.

This is a work in progress. It is intended to be integrated into [Wagtail CMS](https://wagtail.io).

## Usage

### Understanding DraftJS contentState

Unlike traditional rich text editors, DraftJS stores data in a JSON representation.

There are two main parts:

* blocks - lines of data amd inline style attributes (without newlines).
* entityMap – collection of [Entities](https://facebook.github.io/draft-js/docs/advanced-topics-entities.html#content)

For more information, [this article](https://medium.com/@rajaraodv/how-draft-js-represents-rich-text-data-eeabb5f25cf2) covers the concepts in depth.

### Using the exporter

The library requires you to explicity define mappings for the types of blocks and entities you wish to render. We may provide some defaults in the future.


```python
from draftjs_exporter.entities.link import Link
from draftjs_exporter.html import HTML

# Configure your element mappings and entity decorators
config = {
'entity_decorators': {
'LINK': Link()
},
'block_map': {
'header-two': {'element': 'h2'},
'blockquote': {'element': 'blockquote'},
'unstyled': {'element': 'p'}
},
'style_map': {
'ITALIC': {'fontStyle': 'italic'},
'BOLD': {'fontStyle': 'bold'}
}
}

# Initialise the exporter with your configuration
exporter = HTML(config)

# Supply a draftJS `contentState`
content_state = {
'entityMap': {},
'blocks': [
{
'key': '6mgfh',
'text': 'User experience (UX) design',
'type': 'header-two',
'depth': 0,
'inlineStyleRanges': [
{
'offset': 16,
'length': 4,
'style': 'BOLD'
}
],
'entityRanges': []
}
]
}

# Render markup
markup = exporter.call(content_state)
```

### Running the example

You can run an executable example as follows:

```
python ./example.py
```

## Development

### Installation

> Requirements: `virtualenv`, `pyenv`, `twine`

```sh
git clone git@github.com:springload/draftjs_exporter.git
cd draftjs_exporter/
virtualenv .venv
source ./.venv/bin/activate
make init
# Optionally, install the git hooks
./.githooks/deploy
# Optionally, install all tested python versions
pyenv install 2.7.11 && pyenv install 3.3.6 && pyenv install 3.4.4 && pyenv install 3.5.1
pyenv global system 2.7.11 3.3.6 3.4.4 3.5.1
```

### Commands

```sh
make help # See what commands are available.
make init # Install dependencies and initialise for development.
make lint # Lint the project.
make test # Test the project.
make test-watch # Restarts the tests whenever a file changes.
make test-coverage # Run the tests while generating test coverage data.
make test-ci # Continuous integration test suite.
make dev # Restarts the example whenever a file changes.
make clean-pyc # Remove Python file artifacts.
```

### Debugging

- Always run the tests. `npm install -g nodemon`, then `make test_watch`.
- Use a debugger. `pip install ipdb`, then `import ipdb; ipdb.set_trace()`.

---

## R&D notes

### Useful resources

#### Draft.js

- https://medium.com/@rajaraodv/how-draft-js-represents-rich-text-data-eeabb5f25cf2
- https://github.com/nikgraf/awesome-draft-js
- https://github.com/rajaraodv/draftjs-examples
- https://github.com/lokiuz/redraft

### Generating HTML in Python

> https://www.quora.com/What-is-an-easy-to-use-HTML-XML-parser-generator-library-in-Python-similar-to-Hpricot-Nokogiri-in-Ruby

- http://lxml.de/
- https://www.crummy.com/software/BeautifulSoup/
- https://github.com/html5lib/html5lib-python
- http://www.yattag.org/
- http://markup.sourceforge.net/

### Limitations of `lxml`

- Succinct documentation (http://lxml.de/tutorial.html is the best)
- XML parser first, not really meant to generate HTML
- No support for document fragments
- No support for text nodes (`.text` and `.tail` instead)

### Under consideration

- Use https://github.com/icelab/draft-js-ast-exporter for its less ambiguous but also more opinionated exchange format?
- Use https://github.com/html5lib/html5lib-python for its nicer API, but the documentation is even worse than lxml.

---

### Other approaches

We have so far discussed a few different strategies to solve the rendering problem. Below are some of our notes.

> We could create the HTML in JS when you save it in draft, then apply a similar strategy to Wagtail’s Rich Text, by iterating over the output with regexes?
> Still get the benefits of a _much_ improved quality of editor and we can make use of existing JS-based draft exporters.

- `contentState` to JSON, Python doing JSON to HTML, with this contentState -> HTML exporter
- `contentState` to AST to JSON, with AST to HTML in Python
- `contentState` to AST to HTML, with extra Python regexes
- `contentState` to HTML, with extra Python regexes

and it's not just contentState but contentState + some config

#### Constraints (to be weighted)

- Keeping the coupling between the CMS and the editor low so that we can change the editor's capabilities without having to write back-end code
- Giving the CMS enough control over the generated HTML. Not sure if "the CMS" just means "back-end" or if some JS code next to the editor code counts as well.
- Maintaining this over time with changing Draft.js output formats and unmaintained or wildly changing AST formats
- Regardless of implementation, being on the right spot between: "over the lifetime of the site/CMS, a given piece of content should always render the same HTML" and "a piece of content should always render the _best_ HTML"
- Keeping the coupling between CMS and editor low so that we can swap editors easily (bigger scope than the one of a single site/project here)
- Favour logical completeness over raw speed. A regex parser will be faster than building a DOM. But caching makes that mostly irrelevant. The bottlenecks will all likely be in the DB lookups for inline entities anyway.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Files for draftjs-exporter, version 0.1.0
Filename, size File type Python version Upload date Hashes
Filename, size draftjs_exporter-0.1.0.tar.gz (14.6 kB) File type Source Python version None Upload date Hashes View hashes

Supported by

Elastic Elastic Search Pingdom Pingdom Monitoring Google Google BigQuery Sentry Sentry Error logging AWS AWS Cloud computing DataDog DataDog Monitoring Fastly Fastly CDN SignalFx SignalFx Supporter DigiCert DigiCert EV certificate StatusPage StatusPage Status page