Pure-Python Markdown ↔ Atlassian Document Format (ADF) converter with lossless round-tripping
Project description
adflux
A pure-Python converter between Markdown (CommonMark + GFM) and Atlassian Document Format (ADF) — the JSON document model used by Confluence and Jira. Round-trips losslessly, including ADF-only constructs like panels, status badges, mentions, task lists, expand sections, and macros.
flowchart LR
MD[Markdown] -->|markdown-it-py| IR((Internal IR + adf-* envelopes))
ADF[ADF JSON] -->|mapping.yaml| IR
PF[Panflute JSON] -->|pf.load| IR
IR -->|options filter| W{writer}
W -->|CommonMark serializer| MD2[Markdown]
W -->|mapping.yaml| ADF2[ADF JSON]
W -->|pf.dump| PF2[Panflute JSON]
Why adflux
ADF is JSON, but no mature pure-Python tool converts it to and from
Markdown without losing information. adflux fills that gap:
- No system dependencies.
pip install adflux— no Pandoc binary, no Node, no Ruby. - Lossless ADF round-trips. A declarative
mapping.yamlplus a small envelope convention round-trips every ADF construct, including ones the library has never seen. - Idiomatic Markdown output. Panels become GitHub alert blockquotes,
expand sections become
<details>, smart cards become autolinks. The output looks correct in GitHub, VS Code, and any standard viewer. - Conversion options make lossy / lossless behavior explicit and selectable per call.
Install
pip install adflux
Requires Python ≥ 3.11. Linux, macOS, and Windows are CI-tested.
Quick start
from adflux import convert
# Markdown → ADF JSON
adf = convert(open("README.md").read(), src="md", dst="adf")
# ADF JSON → Markdown, dropping ADF-only constructs to plain content
md = convert(adf, src="adf", dst="md", options={"envelopes": "drop"})
adflux convert --from md --to adf README.md > readme.adf.json
adflux convert --from adf --to md page.json
adflux convert --from adf --to md --option envelopes=drop page.json
adflux validate page.adf.json
adflux list-options
Full API and CLI reference: docs/usage.md.
Conversion options
| Option | Choices | Default | Description |
|---|---|---|---|
envelopes |
keep, drop, keep-strict |
keep |
How ADF envelope nodes are handled on lossy targets. |
jira-strict |
true, false |
false |
Reject non-Jira ADF nodes during serialization. |
Worked examples: docs/options.md.
Markdown rendering of ADF constructs
| ADF construct | Rendered as |
|---|---|
panel (info/note/warning/…) |
GitHub alert blockquote (> [!NOTE], > [!TIP], …) |
expand |
<details><summary>title</summary>…</details> |
inlineCard / blockCard / embedCard |
Autolink (<https://…>) |
taskList / taskItem |
GFM task list (- [ ], - [x]) |
emoji, mention |
Plain text (🚀, @alice) |
| Everything else | HTML-comment marker (<!--adf:status …-->) |
The reader recognises every form on its way back, so MD → ADF → MD is
stable.
Supported formats
| Format | Aliases | Description |
|---|---|---|
| Markdown | md, markdown |
CommonMark + GFM via markdown-it-py |
| ADF | adf |
Atlassian Document Format JSON |
| Panflute | panflute, pf |
Pandoc JSON AST for integration with panflute tools |
How it works
- IR: an in-memory document tree built on
panflute AST classes
(used purely as Python data structures — no Pandoc binary involved),
with
Div/Spanenvelopes for ADF-only constructs. - Envelopes: class prefix
adf-<nodeType>plus a base64-JSON blob for complex attributes. A universaladf-rawfallback guarantees zero data loss. - Mapping: every ADF node type is described in
src/adflux/formats/adf/mapping.yaml. Adding a new node type is a YAML edit.
Deeper dives:
docs/design.md ·
docs/architecture.md ·
docs/options.md ·
docs/fidelity-matrix.md ·
docs/extending.md ·
docs/e2e-testing.md.
Examples
Runnable scripts in examples/:
python examples/md_to_adf.py README.md
python examples/adf_to_markdown.py
python examples/confluence_roundtrip.py
Development
git clone https://github.com/mikejhill/adflux
cd adflux
uv sync --all-groups
uv run poe check
Common tasks via poethepoet:
poe test # unit + integration + round-trip + property tests
poe test-e2e # live Confluence + Jira round-trip suite (requires .env)
poe lint # ruff format --check + ruff check
poe typecheck # ty check src
poe cov # pytest with coverage report
poe check # lint + typecheck + test (run before opening a PR)
poe build # sdist + wheel into ./dist
CI runs the full matrix on Linux, macOS, and Windows for Python 3.11–3.13.
Releasing
Releases are automated. Land changes on main using
Conventional Commits, then run
the Release workflow from the GitHub Actions tab. go-semantic-release
computes the next version, builds the sdist + wheel, creates a tagged
GitHub Release, and publishes to PyPI via OIDC trusted publishing.
Contributing
Issues and pull requests are welcome. Before opening a PR:
poe checkmust pass (lint + typecheck + tests).- Use a Conventional Commit message (
feat:,fix:,docs:, …). - New ADF node types need a fixture in
tests/roundtrip/test_node_coverage.py.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file adflux-0.2.0.tar.gz.
File metadata
- Download URL: adflux-0.2.0.tar.gz
- Upload date:
- Size: 68.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
781bf25274cbc5241afc0ea9bbbeb04cf589cf5171598932d5d87e9ee5799d5b
|
|
| MD5 |
dd78dcb3a2dfaa815ae8cbb76d3f4236
|
|
| BLAKE2b-256 |
b56174e9690cd8d524f637e6ff7a71ee938508061d3be87e92bea20635054385
|
Provenance
The following attestation bundles were made for adflux-0.2.0.tar.gz:
Publisher:
release.yml on mikejhill/adflux
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
adflux-0.2.0.tar.gz -
Subject digest:
781bf25274cbc5241afc0ea9bbbeb04cf589cf5171598932d5d87e9ee5799d5b - Sigstore transparency entry: 1335423737
- Sigstore integration time:
-
Permalink:
mikejhill/adflux@597fc4d19e0d4b5239c638778c93aa16b67fc10d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/mikejhill
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@597fc4d19e0d4b5239c638778c93aa16b67fc10d -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file adflux-0.2.0-py3-none-any.whl.
File metadata
- Download URL: adflux-0.2.0-py3-none-any.whl
- Upload date:
- Size: 45.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34db05e85f83d901c6a9442314d4c64a013555d78816b7bb860032064fbbe566
|
|
| MD5 |
394625a16a01fe4d644c3151f78bcebb
|
|
| BLAKE2b-256 |
72062aaa4adecc8537e9b271469abcbc4d0b333781c3d1eb20ffeb9e7a476aac
|
Provenance
The following attestation bundles were made for adflux-0.2.0-py3-none-any.whl:
Publisher:
release.yml on mikejhill/adflux
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
adflux-0.2.0-py3-none-any.whl -
Subject digest:
34db05e85f83d901c6a9442314d4c64a013555d78816b7bb860032064fbbe566 - Sigstore transparency entry: 1335423857
- Sigstore integration time:
-
Permalink:
mikejhill/adflux@597fc4d19e0d4b5239c638778c93aa16b67fc10d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/mikejhill
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@597fc4d19e0d4b5239c638778c93aa16b67fc10d -
Trigger Event:
workflow_dispatch
-
Statement type: