Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f85c9ad
Add Markdown importer design spec and implementation plan
thibaudcolas Jul 23, 2026
e5a3713
Add MarkdownParseError exception type
thibaudcolas Jul 23, 2026
a169876
Add entity resolvers for Markdown import
thibaudcolas Jul 23, 2026
1618b13
Add ContentState builder for Markdown parser
thibaudcolas Jul 23, 2026
5e8e2d6
Add inline parser: escapes, code spans, hard breaks
thibaudcolas Jul 23, 2026
61c90ce
Add inline parser emphasis support
thibaudcolas Jul 23, 2026
59f582f
Add inline parser link and image tests
thibaudcolas Jul 23, 2026
851f2c0
Add inline HTML style whitelist to inline parser
thibaudcolas Jul 23, 2026
baaf594
Add block parser: paragraphs, headings, thematic breaks
thibaudcolas Jul 23, 2026
a2e1148
Add block parser blockquotes and fenced code
thibaudcolas Jul 23, 2026
dc0b646
Add block parser lists with depth tracking
thibaudcolas Jul 23, 2026
0e70879
Add standalone image atomic blocks and parser toggle tests
thibaudcolas Jul 23, 2026
ecb2ff0
Format Markdown parser modules
thibaudcolas Jul 23, 2026
e30f508
Add MarkdownParser with parser config
thibaudcolas Jul 23, 2026
46b268d
Suppress ty false positive in parser test
thibaudcolas Jul 23, 2026
09e7d68
Add ContentStateFilter with declarative rules
thibaudcolas Jul 23, 2026
33fd30c
Fix type annotations in ContentStateFilter
thibaudcolas Jul 23, 2026
e360e3f
Add MarkdownImporter public API
thibaudcolas Jul 23, 2026
d8736d2
Add importer snapshot tests with fixture round-trips
thibaudcolas Jul 23, 2026
c106c7a
Fix fixture importer typing
thibaudcolas Jul 23, 2026
87b8bea
Restore ImporterConfig import in snapshot tests
thibaudcolas Jul 23, 2026
f83b64a
Add importer and filter property-based tests
thibaudcolas Jul 23, 2026
0d23d4d
Add Markdown import demo to example.py
thibaudcolas Jul 23, 2026
1967a06
Document the Markdown importer
thibaudcolas Jul 23, 2026
819af94
Polish Markdown importer for release
thibaudcolas Jul 23, 2026
37d012f
Format importer test modules
thibaudcolas Jul 23, 2026
44e4e48
Annotate ContentState in filter edge case test
thibaudcolas Jul 23, 2026
7505b36
Show markdown diffing in example.py
thibaudcolas Jul 24, 2026
b77c524
Apply code review fixes: tab stops, depth guard, demote guard, docs
thibaudcolas Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 46 additions & 31 deletions .agents/skills/draftjs-exporter/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: draftjs-exporter
description: 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`, `markdown_to_content_state`, or Draft.js `ContentState` / `entityMap` JSON.
description: 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.
license: MIT
metadata:
version: "6.0.0"
Expand All @@ -16,25 +16,27 @@ The public API is small: an `HTML` exporter class, a `DOM` namespace with a Reac

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](https://wagtail.github.io/draftjs_exporter/getting-started/) |
| Override default blocks / styles | `**BLOCK_MAP`, `**STYLE_MAP` then override keys | [block map](https://wagtail.github.io/draftjs_exporter/configuration/#block-map) |
| Add HTML attributes to a block | `BLOCK_TYPES.X: {"element": "h3", "props": {"class": "…"}}` | [block map](https://wagtail.github.io/draftjs_exporter/configuration/#block-map) |
| Wrap adjacent blocks (lists) | `"wrapper": "ul", "wrapper_props": {"class": "…"}` | [block map](https://wagtail.github.io/draftjs_exporter/configuration/#block-map) |
| Render entity data (image, link) | `"entity_decorators": {ENTITY_TYPES.LINK: link}` | [entity components](https://wagtail.github.io/draftjs_exporter/custom-components/#entity-components) |
| 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) |
| Compose components / pass children | `DOM.create_element(type, props, *children)` | [nesting](https://wagtail.github.io/draftjs_exporter/custom-components/#nesting-and-reusing-components) |
| Replace text by regex (line breaks, mentions) | `"composite_decorators": [{"strategy": rx, "component": fn}]` | [composite decorators](https://wagtail.github.io/draftjs_exporter/configuration/#composite-decorators) |
| Handle missing block / style / entity types | `BLOCK_TYPES.FALLBACK`, `INLINE_STYLES.FALLBACK`, `ENTITY_TYPES.FALLBACK` | [fallbacks](https://wagtail.github.io/draftjs_exporter/fallback-components/) |
| Discard an entity type | `ENTITY_TYPES.EMBED: None` | [entity decorators](https://wagtail.github.io/draftjs_exporter/configuration/#entity-decorators) |
| Switch DOM engine | `"engine": DOM.HTML5LIB` (or `DOM.LXML`, `DOM.STRING_COMPAT`) | [alternative engines](https://wagtail.github.io/draftjs_exporter/alternative-engines/) |
| Render Markdown instead of HTML | `HTML(MARKDOWN_CONFIG).render(content_state)` | [Markdown](https://wagtail.github.io/draftjs_exporter/markdown/) |
| Customize Markdown characters | `build_markdown_config({"bold": "__", "italic": "*", ...})` | [Markdown chars](https://wagtail.github.io/draftjs_exporter/markdown/#configuring-output-characters) |
| Parse Markdown back into ContentState | `markdown_to_content_state(markdown, options=None)` | [Markdown importer](https://wagtail.github.io/draftjs_exporter/markdown/#importer) |
| Debug output | `DOM.render_debug(elt)`, `echo '{...}' \| python example.py -` | [API](https://wagtail.github.io/draftjs_exporter/api/) |
| Migrate between major versions | `DOM.STRING_COMPAT` for old `string` output; per-version notes | [migration guide](https://wagtail.github.io/draftjs_exporter/migration-guide/) |
| Full public API (constants, types, defaults) | `draftjs_exporter.constants`, `types`, `defaults` | [API reference](https://wagtail.github.io/draftjs_exporter/api/) |
| Task | Solution | Docs |
| --------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Render ContentState to HTML | `HTML({}).render(content_state)` | [getting-started](https://wagtail.github.io/draftjs_exporter/getting-started/) |
| Override default blocks / styles | `**BLOCK_MAP`, `**STYLE_MAP` then override keys | [block map](https://wagtail.github.io/draftjs_exporter/configuration/#block-map) |
| Add HTML attributes to a block | `BLOCK_TYPES.X: {"element": "h3", "props": {"class": "…"}}` | [block map](https://wagtail.github.io/draftjs_exporter/configuration/#block-map) |
| Wrap adjacent blocks (lists) | `"wrapper": "ul", "wrapper_props": {"class": "…"}` | [block map](https://wagtail.github.io/draftjs_exporter/configuration/#block-map) |
| Render entity data (image, link) | `"entity_decorators": {ENTITY_TYPES.LINK: link}` | [entity components](https://wagtail.github.io/draftjs_exporter/custom-components/#entity-components) |
| 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) |
| Compose components / pass children | `DOM.create_element(type, props, *children)` | [nesting](https://wagtail.github.io/draftjs_exporter/custom-components/#nesting-and-reusing-components) |
| Replace text by regex (line breaks, mentions) | `"composite_decorators": [{"strategy": rx, "component": fn}]` | [composite decorators](https://wagtail.github.io/draftjs_exporter/configuration/#composite-decorators) |
| Handle missing block / style / entity types | `BLOCK_TYPES.FALLBACK`, `INLINE_STYLES.FALLBACK`, `ENTITY_TYPES.FALLBACK` | [fallbacks](https://wagtail.github.io/draftjs_exporter/fallback-components/) |
| Discard an entity type | `ENTITY_TYPES.EMBED: None` | [entity decorators](https://wagtail.github.io/draftjs_exporter/configuration/#entity-decorators) |
| Switch DOM engine | `"engine": DOM.HTML5LIB` (or `DOM.LXML`, `DOM.STRING_COMPAT`) | [alternative engines](https://wagtail.github.io/draftjs_exporter/alternative-engines/) |
| Render Markdown instead of HTML | `HTML(MARKDOWN_CONFIG).render(content_state)` | [Markdown](https://wagtail.github.io/draftjs_exporter/markdown/) |
| Customize Markdown characters | `build_markdown_config({"bold": "__", "italic": "*", ...})` | [Markdown chars](https://wagtail.github.io/draftjs_exporter/markdown/#configuring-output-characters) |
| Import Markdown as ContentState | `MarkdownImporter({}).import_markdown(md)` | [Markdown importer](https://wagtail.github.io/draftjs_exporter/markdown-importer/) |
| 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) |
| Filter imported content (demote headings) | `ContentStateFilter([{"type": "block", "match": ..., "action": "demote"}])` | [Filtering](https://wagtail.github.io/draftjs_exporter/markdown-importer/#filtering) |
| Debug output | `DOM.render_debug(elt)`, `echo '{...}' \| python example.py -` | [API](https://wagtail.github.io/draftjs_exporter/api/) |
| Migrate between major versions | `DOM.STRING_COMPAT` for old `string` output; per-version notes | [migration guide](https://wagtail.github.io/draftjs_exporter/migration-guide/) |
| Full public API (constants, types, defaults) | `draftjs_exporter.constants`, `types`, `defaults` | [API reference](https://wagtail.github.io/draftjs_exporter/api/) |

## Quick start

Expand Down Expand Up @@ -182,20 +184,29 @@ All defaults produce valid [CommonMark](https://commonmark.org/). Limitations: n

### Importer

The Markdown importer (`markdown_to_content_state`) parses Markdown back into Draft.js `ContentState`, enabling round-trip workflows (`ContentState → Markdown → ContentState`). It is dependency-free and recognizes the same Markdown subset the exporter produces.
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.

```python
from draftjs_exporter import build_markdown_config, HTML, markdown_to_content_state

options = {"bold": "__", "italic": "*"}
config = build_markdown_config(options)
exporter = HTML(config)
markdown = exporter.render(content_state)
# Round-trip by passing the same options to the importer.
round_tripped = markdown_to_content_state(markdown, options)
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)
```

Only inline style markers (`bold`, `italic`, `strikethrough`) and `html_style_tags` are configurable. Block-level syntax (list markers, horizontal rules, code fences, ordered-list delimiters) is recognized polymorphically. See [Markdown importer](https://wagtail.github.io/draftjs_exporter/markdown/#importer).
See [Markdown importer](https://wagtail.github.io/draftjs_exporter/markdown-importer/).

## Common gotchas

Expand All @@ -216,7 +227,11 @@ All imported from `draftjs_exporter` directly:
- **`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`.
- **Constants**: `BLOCK_TYPES`, `INLINE_STYLES`, `ENTITY_TYPES` (each with a `FALLBACK` member).
- **Markdown helpers**: `build_markdown_config(options)`, `markdown_to_content_state(markdown, options=None)`, plus option type aliases `MarkdownOptions` and `MarkdownImporterOptions`.
- **Markdown helper**: `build_markdown_config(options)`, plus the option type alias `MarkdownOptions`.
- **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).

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

## [Unreleased]

### Added

- Add experimental Markdown importer: `MarkdownImporter` converts Markdown to Draft.js ContentState, with configurable entity resolvers (`scheme_resolver` for internal URL schemes), an inline HTML style whitelist, and `ContentStateFilter` for content policy.

## [v6.0.0](https://github.com/wagtail/draftjs_exporter/releases/tag/v6.0.0)

### Added
Expand Down
Loading
Loading