Note: This library is under active development and not yet stable. API may change.
A PHP library that converts between GitHub Flavored Markdown (GFM) and Atlassian Document Format (ADF) for use with Jira and Confluence APIs. Supports both directions: Markdown → ADF and ADF → Markdown.
Inspired by marklassian (JavaScript).
composer require kalusek/markdown-to-adfuse Kalusek\MarkdownToAdf\MarkdownToAdf;
$adf = MarkdownToAdf::convert('# Hello **World**');
echo json_encode($adf, JSON_PRETTY_PRINT);Output:
{
"version": 1,
"type": "doc",
"content": [
{
"type": "heading",
"attrs": { "level": 1 },
"content": [
{ "type": "text", "text": "Hello " },
{ "type": "text", "text": "World", "marks": [{ "type": "strong" }] }
]
}
]
}use Kalusek\MarkdownToAdf\AdfToMarkdown;
$adf = [
'version' => 1,
'type' => 'doc',
'content' => [
['type' => 'heading', 'attrs' => ['level' => 2], 'content' => [['type' => 'text', 'text' => 'Problem']]],
['type' => 'paragraph', 'content' => [
['type' => 'text', 'text' => 'The table is '],
['type' => 'text', 'text' => 'too wide', 'marks' => [['type' => 'strong']]],
['type' => 'text', 'text' => '.'],
]],
],
];
echo AdfToMarkdown::convert($adf);Output:
## Problem
The table is **too wide**.- Headings (H1-H6)
- Paragraphs and line breaks (hard + soft)
- Bold, italic, strikethrough, inline code
- Links and images (images become block-level
mediaSinglenodes) - Fenced and indented code blocks with language
- Ordered and unordered lists with nesting
- Blockquotes
- Horizontal rules
- Tables
- GFM task lists (
- [ ]/- [x]) with nesting - Raw ADF embedding via
<adf>tags
- Headings, paragraphs, hard breaks
- Bold, italic, strikethrough, inline code, links, underline
- Bullet lists, ordered lists, nested lists
- Task lists (
TODO/DONE) - Fenced code blocks with language
- Blockquotes and panels
- Horizontal rules
- Tables (with header rows)
- Images (
mediaSingle/media) - Mentions, emoji, inline cards
For more control, instantiate the class:
$converter = new MarkdownToAdf(debug: true, bail: false);
$adf = $converter->parse($markdown);| Option | Type | Default | Description |
|---|---|---|---|
debug |
bool |
false |
Emit E_USER_WARNING when an unrecognized node type is encountered |
bail |
bool |
false |
Throw RuntimeException when an unrecognized node type is encountered |
Register handlers for CommonMark node types you want to convert differently, or for custom extension nodes:
use League\CommonMark\Node\Node;
$converter = new MarkdownToAdf();
// Override how headings are converted
$converter->addHandler(Heading::class, function (Node $node) {
return [[
'type' => 'heading',
'attrs' => ['level' => min($node->getLevel(), 3)], // cap at H3
'content' => [['type' => 'text', 'text' => 'Custom: ' . $node->getLevel()]],
]];
});
$adf = $converter->parse($markdown);Handlers receive a Node and must return list<array> (an array of ADF node arrays).
The processChildren() and processInlines() methods are public so custom handlers can recurse into child content:
$converter->addHandler(MyBlock::class, function (Node $node) use ($converter) {
return [[
'type' => 'panel',
'attrs' => ['panelType' => 'info'],
'content' => $converter->processChildren($node),
]];
});Embed custom ADF nodes directly in Markdown using <adf> tags:
# My page
<adf>
{"type":"extension","attrs":{"extensionType":"com.atlassian.confluence.macro.core","extensionKey":"status","parameters":{"macroParams":{"title":{"value":"Done"},"colour":{"value":"Green"}}}}}
</adf>
More content after the macro.Content must be valid JSON: either a single object or an array of objects, each with a "type" property.
- Empty input returns a valid empty ADF document:
{"version": 1, "type": "doc", "content": []} - Mixed task lists (regular items + task items in the same list) are treated as regular bullet lists, since ADF
taskListnodes don't support non-task children - Inline code marks only combine with
linkmarks per the ADF spec. Other marks (bold, italic, strikethrough) are stripped when inside inline code.
- PHP 8.1+
league/commonmark^2.6
MIT