{"id":13758284,"url":"https://github.com/springload/draftjs_exporter","last_synced_at":"2025-05-16T05:05:01.554Z","repository":{"id":9560475,"uuid":"62028497","full_name":"springload/draftjs_exporter","owner":"springload","description":"Convert Draft.js ContentState to HTML","archived":false,"fork":false,"pushed_at":"2025-02-21T17:36:36.000Z","size":750,"stargazers_count":84,"open_issues_count":3,"forks_count":21,"subscribers_count":15,"default_branch":"main","last_synced_at":"2025-05-13T04:41:21.049Z","etag":null,"topics":["draft-js","draftail","draftjs-exporter","exporter","python","rich-text"],"latest_commit_sha":null,"homepage":"https://www.draftail.org/blog/2018/03/13/rethinking-rich-text-pipelines-with-draft-js","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/springload.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"docs/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"docs/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"docs/SECURITY.md","support":"docs/SUPPORT.md","governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-06-27T05:45:36.000Z","updated_at":"2025-04-02T07:39:53.000Z","dependencies_parsed_at":"2024-02-09T22:45:36.669Z","dependency_job_id":null,"html_url":"https://github.com/springload/draftjs_exporter","commit_stats":{"total_commits":599,"total_committers":12,"mean_commits":"49.916666666666664","dds":0.04006677796327207,"last_synced_commit":"7669134838c681cfc37e68a325727f1564c640e8"},"previous_names":[],"tags_count":36,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/springload%2Fdraftjs_exporter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/springload%2Fdraftjs_exporter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/springload%2Fdraftjs_exporter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/springload%2Fdraftjs_exporter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/springload","download_url":"https://codeload.github.com/springload/draftjs_exporter/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254471061,"owners_count":22076585,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["draft-js","draftail","draftjs-exporter","exporter","python","rich-text"],"created_at":"2024-08-03T13:00:24.430Z","updated_at":"2025-05-16T05:05:01.526Z","avatar_url":"https://github.com/springload.png","language":"Python","readme":"# [Draft.js exporter](https://pypi.org/project/draftjs_exporter/)\n\n\u003e Library to convert rich text from Draft.js raw ContentState to HTML.\n\nIt is developed alongside the [Draftail](https://www.draftail.org/) rich text editor, for [Wagtail](https://github.com/wagtail/wagtail). Check out the [online demo](http://playground.draftail.org/), and our [introductory blog post](https://www.draftail.org/blog/2018/03/13/rethinking-rich-text-pipelines-with-draft-js).\n\n## Why\n\n[Draft.js](https://draftjs.org/) is a rich text editor framework for [React](https://reactjs.org/). 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.\n\nThe 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](https://www.draftail.org/blog/2018/03/13/rethinking-rich-text-pipelines-with-draft-js).\n\n## Features\n\nThis project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html), and [measures performance](https://thib.me/python-memory-profiling-for-the-draft-js-exporter) and [code coverage](https://coveralls.io/github/springload/draftjs_exporter). Code is checked with [mypy](https://mypy.readthedocs.io/en/latest/index.html).\n\n- Extensive configuration of the generated HTML.\n- Default, extensible block \u0026 inline style maps for common HTML elements.\n- Convert line breaks to `\u003cbr\u003e` elements.\n- Define any attribute in the block map – custom class names for elements.\n- React-like API to create custom components.\n- Automatic conversion of entity data to HTML attributes (int \u0026 boolean to string, style object to style string).\n- Nested lists (`\u003cli\u003e` elements go inside `\u003cul\u003e` or `\u003col\u003e`, with multiple levels).\n- Output inline styles as inline elements (`\u003cem\u003e`, `\u003cstrong\u003e`, pick and choose, with any attribute).\n- Overlapping inline style and entity ranges.\n- Static type annotations.\n\n## Usage\n\nDraft.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](https://medium.com/@rajaraodv/how-draft-js-represents-rich-text-data-eeabb5f25cf2) covers the concepts further.\n\n### Getting started\n\nThis exporter takes the Draft.js ContentState data as input, and outputs HTML based on its configuration. To get started, install the package:\n\n```sh\npip install draftjs_exporter\n```\n\nWe support the following Python versions: 3.9, 3.10, 3.11, 3.12, 3.13, 3.14. For legacy Python versions, find compatible releases in the [CHANGELOG](https://github.com/springload/draftjs_exporter/blob/main/CHANGELOG.md).\n\nIn your code, create an exporter and use the `render` method to create HTML:\n\n```python\nfrom draftjs_exporter.dom import DOM\nfrom draftjs_exporter.html import HTML\n\n# Configuration options are detailed below.\nconfig = {}\n\n# Initialize the exporter.\nexporter = HTML(config)\n\n# Render a Draft.js `contentState`\nhtml = exporter.render({\n    'entityMap': {},\n    'blocks': [{\n        'key': '6m5fh',\n        'text': 'Hello, world!',\n        'type': 'unstyled',\n        'depth': 0,\n        'inlineStyleRanges': [],\n        'entityRanges': []\n    }]\n})\n\nprint(html)\n```\n\nYou can also run an example by downloading this repository and then using `python example.py`, or by using our [online Draft.js demo](http://playground.draftail.org/).\n\n### Configuration\n\nThe exporter output is extensively configurable to cater for varied rich text requirements.\n\n```python\n# draftjs_exporter provides default configurations and predefined constants for reuse.\nfrom draftjs_exporter.constants import BLOCK_TYPES, ENTITY_TYPES\nfrom draftjs_exporter.defaults import BLOCK_MAP, STYLE_MAP\nfrom draftjs_exporter.dom import DOM\n\nconfig = {\n    # `block_map` is a mapping from Draft.js block types to a definition of their HTML representation.\n    # Extend BLOCK_MAP to start with sane defaults, or make your own from scratch.\n    'block_map': dict(BLOCK_MAP, **{\n        # The most basic mapping format, block type to tag name.\n        BLOCK_TYPES.HEADER_TWO: 'h2',\n        # Use a dict to define props on the block.\n        BLOCK_TYPES.HEADER_THREE: {'element': 'h3', 'props': {'class': 'u-text-center'}},\n        # Add a wrapper (and wrapper_props) to wrap adjacent blocks.\n        BLOCK_TYPES.UNORDERED_LIST_ITEM: {\n            'element': 'li',\n            'wrapper': 'ul',\n            'wrapper_props': {'class': 'bullet-list'},\n        },\n        # Use a custom component for more flexibility (reading block data or depth).\n        BLOCK_TYPES.BLOCKQUOTE: blockquote,\n        BLOCK_TYPES.ORDERED_LIST_ITEM: {\n            'element': list_item,\n            'wrapper': ordered_list,\n        },\n        # Provide a fallback component (advanced).\n        BLOCK_TYPES.FALLBACK: block_fallback\n    }),\n    # `style_map` defines the HTML representation of inline elements.\n    # Extend STYLE_MAP to start with sane defaults, or make your own from scratch.\n    'style_map': dict(STYLE_MAP, **{\n        # Use the same mapping format as in the `block_map`.\n        'KBD': 'kbd',\n        # The `style` prop can be defined as a dict, that will automatically be converted to a string.\n        'HIGHLIGHT': {'element': 'strong', 'props': {'style': {'textDecoration': 'underline'}}},\n        # Provide a fallback component (advanced).\n        INLINE_STYLES.FALLBACK: style_fallback,\n    }),\n    'entity_decorators': {\n        # Map entities to components so they can be rendered with their data.\n        ENTITY_TYPES.IMAGE: image,\n        ENTITY_TYPES.LINK: link\n        # Lambdas work too.\n        ENTITY_TYPES.HORIZONTAL_RULE: lambda props: DOM.create_element('hr'),\n        # Discard those entities.\n        ENTITY_TYPES.EMBED: None,\n        # Provide a fallback component (advanced).\n        ENTITY_TYPES.FALLBACK: entity_fallback,\n    },\n    'composite_decorators': [\n        # Use composite decorators to replace text based on a regular expression.\n        {\n            'strategy': re.compile(r'\\n'),\n            'component': br,\n        },\n        {\n            'strategy': re.compile(r'#\\w+'),\n            'component': hashtag,\n        },\n        {\n            'strategy': LINKIFY_RE,\n            'component': linkify,\n        },\n    ],\n}\n```\n\nSee [examples.py](example.py) for more details.\n\n## Advanced usage\n\n### Custom components\n\nTo generate arbitrary markup with dynamic data, the exporter comes with an API to create rendering components. This API mirrors React’s [createElement](https://facebook.github.io/react/docs/top-level-api.html#react.createelement) API (what JSX compiles to).\n\n```python\n# All of the API is available from a single `DOM` namespace\nfrom draftjs_exporter.dom import DOM\n\n\n# Components are simple functions that take `props` as parameter and return DOM elements.\ndef image(props):\n    # This component creates an image element, with the relevant attributes.\n    return DOM.create_element('img', {\n        'src': props.get('src'),\n        'width': props.get('width'),\n        'height': props.get('height'),\n        'alt': props.get('alt'),\n    })\n\n\ndef blockquote(props):\n    # This component uses block data to render a blockquote.\n    block_data = props['block']['data']\n\n    # Here, we want to display the block's content so we pass the `children` prop as the last parameter.\n    return DOM.create_element('blockquote', {\n        'cite': block_data.get('cite')\n    }, props['children'])\n\n\ndef button(props):\n    href = props.get('href', '#')\n    icon_name = props.get('icon', None)\n    text = props.get('text', '')\n\n    return DOM.create_element('a', {\n            'class': 'icon-text' if icon_name else None,\n            'href': href,\n        },\n        # There can be as many `children` as required.\n        # It is also possible to reuse other components and render them instead of HTML tags.\n        DOM.create_element(icon, {'name': icon_name}) if icon_name else None,\n        DOM.create_element('span', {'class': 'icon-text'}, text) if icon_name else text\n    )\n```\n\nApart from `create_element`, a `parse_html` method is also available. Use it to interface with other HTML generators, like template engines – at your own risk: that method does not sanitize the input.\n\nSee `examples.py` in the repository for more details.\n\n### Fallback components\n\nWhen dealing with changes in the content schema, as part of ongoing development or migrations, some content can go stale.\nTo solve this, the exporter allows the definition of fallback components for blocks, styles, and entities.\nThis 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.\nPlease get in touch!\n\nAdd the following to the exporter config,\n\n```python\nconfig = {\n    'block_map': dict(BLOCK_MAP, **{\n        # Provide a fallback for block types.\n        BLOCK_TYPES.FALLBACK: block_fallback\n    }),\n}\n```\n\nThis fallback component can now control the exporter behavior when normal components are not found. Here is an example:\n\n```python\ndef block_fallback(props):\n    type_ = props['block']['type']\n\n    if type_ == 'example-discard':\n        logging.warning(f'Missing config for \"{type_}\". Discarding block, keeping content.')\n        # Directly return the block's children to keep its content.\n        return props['children']\n    elif type_ == 'example-delete':\n        logging.error(f'Missing config for \"{type_}\". Deleting block.')\n        # Return None to not render anything, removing the whole block.\n        return None\n    else:\n        logging.warning(f'Missing config for \"{type_}\". Using div instead.')\n        # Provide a fallback.\n        return DOM.create_element('div', {}, props['children'])\n```\n\nSee `examples.py` in the repository for more details.\n\n### Alternative backing engines\n\nBy default, the exporter uses a dependency-free engine called `string` to build the DOM tree. There are alternatives:\n\n- `html5lib` (via BeautifulSoup)\n- `lxml`.\n- `string_compat` (A variant of `string` with no backwards-incompatible changes since its first release).\n\nThe `string` engine is the fastest, and does not have any dependencies. Its only drawback is that the `parse_html` method does not escape/sanitize HTML like that of other engines.\n\n- For `html5lib`, do `pip install draftjs_exporter[html5lib]`.\n- For `lxml`, do `pip install draftjs_exporter[lxml]`. It also requires `libxml2` and `libxslt` to be available on your system.\n- There are no additional dependencies for `string_compat`.\n\nThen, use the `engine` attribute of the exporter config:\n\n```python\nconfig = {\n    # Specify which DOM backing engine to use.\n    'engine': DOM.HTML5LIB,\n    # Or for lxml:\n    'engine': DOM.LXML,\n    # Or to use the \"maximum output stability\" string_compat engine:\n    'engine': DOM.STRING_COMPAT,\n}\n```\n\nTo use the `lxml` or `html5lib` engines with arbitrary versions of those dependencies, simply install `draftjs_exporter` without the extras, and separately install the desired versions of the dependencies.\n\n### Custom backing engines\n\nThe 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)](https://github.com/thibaudcolas/draftjs_exporter_markdown).\n\nHere is an example implementation:\n\n```python\nfrom draftjs_exporter import DOMEngine\n\nclass DOMListTree(DOMEngine):\n    \"\"\"\n    Element tree using nested lists.\n    \"\"\"\n\n    @staticmethod\n    def create_tag(t, attr=None):\n        return [t, attr, []]\n\n    @staticmethod\n    def append_child(elt, child):\n        elt[2].append(child)\n\n    @staticmethod\n    def render(elt):\n        return elt\n\n\nexporter = HTML({\n    # Use the dotted module syntax to point to the DOMEngine implementation.\n    'engine': 'my_project.example.DOMListTree'\n})\n```\n\n### Type annotations\n\nThe exporter’s codebase uses static type annotations, checked with mypy. Reusable types are made available:\n\n```python\nfrom draftjs_exporter.dom import DOM\nfrom draftjs_exporter.types import Element, Props\n\n\n# Components are simple functions that take `props` as parameter and return DOM elements.\ndef image(props: Props) -\u003e Element:\n    # This component creates an image element, with the relevant attributes.\n    return DOM.create_element('img', {\n        'src': props.get('src'),\n        'width': props.get('width'),\n        'height': props.get('height'),\n        'alt': props.get('alt'),\n    })\n```\n\n## Contributing\n\nSee 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](docs/CONTRIBUTING.md).\n\nIf you just want to set up the project on your own computer, the contribution guidelines also contain all of the setup commands.\n\n## Credits\n\nThis project is made possible by the work of [Springload](https://github.com/springload), a New Zealand digital agency. The _beautiful_ demo site is the work of [@thibaudcolas](https://github.com/thibaudcolas).\n\nView the full list of [contributors](https://github.com/springload/draftjs_exporter/graphs/contributors). [MIT licensed](LICENSE).\n","funding_links":[],"categories":["Common Utilities"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspringload%2Fdraftjs_exporter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspringload%2Fdraftjs_exporter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspringload%2Fdraftjs_exporter/lists"}