Meziantou.Framework.AtlassianDataFormat
1.0.2
Prefix Reserved
dotnet add package Meziantou.Framework.AtlassianDataFormat --version 1.0.2
NuGet\Install-Package Meziantou.Framework.AtlassianDataFormat -Version 1.0.2
<PackageReference Include="Meziantou.Framework.AtlassianDataFormat" Version="1.0.2" />
<PackageVersion Include="Meziantou.Framework.AtlassianDataFormat" Version="1.0.2" />
<PackageReference Include="Meziantou.Framework.AtlassianDataFormat" />
paket add Meziantou.Framework.AtlassianDataFormat --version 1.0.2
#r "nuget: Meziantou.Framework.AtlassianDataFormat, 1.0.2"
#:package Meziantou.Framework.AtlassianDataFormat@1.0.2
#addin nuget:?package=Meziantou.Framework.AtlassianDataFormat&version=1.0.2
#tool nuget:?package=Meziantou.Framework.AtlassianDataFormat&version=1.0.2
Meziantou.Framework.AtlassianDataFormat
Meziantou.Framework.AtlassianDataFormat provides:
- A typed object model for the Atlassian Document Format (ADF), the JSON rich-text format used by Jira, Confluence, and Bitbucket (
AdfDocument,AdfNode,AdfMark, ...) - A Markdown converter with per-node options for the ADF constructs Markdown cannot express (
AdfToMarkdown)
Convert a document to Markdown
The types are in the Meziantou.Framework.AtlassianDataFormat namespace.
var json = """
{
"version": 1,
"type": "doc",
"content": [
{ "type": "heading", "attrs": { "level": 1 }, "content": [ { "type": "text", "text": "Title" } ] },
{ "type": "paragraph", "content": [ { "type": "text", "text": "Hello", "marks": [ { "type": "strong" } ] } ] }
]
}
""";
var markdown = AdfToMarkdown.Convert(json);
Assert.Equal("# Title\n\n**Hello**", markdown);
Convert also accepts a JsonElement or a JsonNode, which avoids a round trip when the document
comes from an API response you already parsed.
Configure the conversion
Defaults produce plain CommonMark/GitHub-Flavored Markdown; HTML output is opt-in. With the json of the previous example:
var markdown = AdfToMarkdown.Convert(json, new AdfToMarkdownOptions
{
PanelStyle = AdfPanelStyle.GitHubAlert, // > [!WARNING]
ExpandStyle = AdfExpandStyle.HtmlDetails, // <details><summary>…</summary>
TableStyle = AdfTableStyle.Auto, // pipe table, or HTML when cells span rows/columns
MediaRendering = AdfMediaRendering.Link,
EmojiRendering = AdfEmojiRendering.ShortName,
TaskListStyle = AdfTaskListStyle.Checkbox,
DecisionListStyle = AdfDecisionListStyle.BulletList,
UnknownNodeHandling = AdfUnknownNodeHandling.KeepContent,
MentionFormat = "@{text}",
StatusFormat = "**[{text}]**",
DateFormat = "yyyy-MM-dd",
HeadingStyle = AdfHeadingStyle.Atx,
EmphasisMarker = AdfEmphasisMarker.Asterisk,
CodeBlockStyle = AdfCodeBlockStyle.Fenced,
LineBreakStyle = AdfLineBreakStyle.Backslash,
});
Resolving media and mentions
Only media of type external carries a URL. Files stored by Atlassian carry an identifier and a
collection, and turning those into a URL needs an authenticated call to the media API. Documents
returned by the APIs also often omit the display name of a mention. Both can be supplied by a
callback:
var json = """
{
"version": 1,
"type": "doc",
"content": [
{ "type": "paragraph", "content": [ { "type": "mention", "attrs": { "id": "123" } } ] },
{
"type": "mediaSingle",
"content": [ { "type": "media", "attrs": { "type": "file", "id": "abc", "collection": "files", "alt": "diagram.png" } } ]
}
]
}
""";
var displayNames = new Dictionary<string, string> { ["123"] = "Alex" };
var markdown = AdfToMarkdown.Convert(json, new AdfToMarkdownOptions
{
MediaUrlResolver = media => $"https://media.example.com/{media.Collection}/{media.Id}",
MentionResolver = mention => displayNames.GetValueOrDefault(mention.Id),
});
Assert.Equal("@Alex\n\n", markdown);
Work with the object model
var json = """
{
"version": 1,
"type": "doc",
"content": [
{ "type": "heading", "attrs": { "level": 1 }, "content": [ { "type": "text", "text": "Title" } ] },
{ "type": "paragraph", "content": [ { "type": "text", "text": "Hello " }, { "type": "mention", "attrs": { "id": "123", "text": "@Alex" } } ] }
]
}
""";
var document = AdfDocument.Parse(json);
var mentionIds = document.Descendants().OfType<AdfMention>().Select(mention => mention.Id).ToList();
Assert.Equal(["123"], mentionIds);
var heading = (AdfHeading)document.Content[0];
Assert.Equal(1, heading.Level);
var markdown = document.ToMarkdown();
Assert.Equal("# Title\n\nHello @Alex", markdown);
var roundTripped = document.ToJsonString();
Assert.Equal(AdfDocument.Parse(roundTripped).ToMarkdown(), markdown);
Documents can also be built from scratch:
var document = new AdfDocument
{
Content =
[
new AdfHeading { Level = 1, Content = [new AdfText { Text = "Title" }] },
new AdfParagraph { Content = [new AdfText { Text = "Hello", Marks = [new AdfStrongMark()] }] },
],
};
var json = document.ToJsonString();
Assert.Equal("""{"version":1,"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"Title"}]},{"type":"paragraph","content":[{"type":"text","text":"Hello","marks":[{"type":"strong"}]}]}]}""", json);
Unknown nodes
Atlassian adds node types regularly, and real documents contain nodes such as unsupportedBlock.
Parsing never fails on an unknown type: it produces an AdfUnknownNode that keeps the original JSON,
so the document round-trips unchanged. AdfToMarkdownOptions.UnknownNodeHandling controls whether
such a node is skipped, has its content converted, or throws.
Supported nodes and marks
Nodes: blockCard, blockquote, bodiedExtension, bulletList, caption, codeBlock, date,
decisionItem, decisionList, embedCard, emoji, expand, extension, hardBreak, heading,
inlineCard, inlineExtension, layoutColumn, layoutSection, listItem, media, mediaGroup,
mediaInline, mediaSingle, mention, nestedExpand, orderedList, panel, paragraph,
placeholder, rule, status, table, tableCell, tableHeader, tableRow, taskItem,
taskList, text.
Marks: alignment, annotation, backgroundColor, border, breakout, code, em,
indentation, link, strike, strong, subsup, textColor, underline.
Marks with no Markdown equivalent — underline, textColor, backgroundColor, annotation,
border, alignment, indentation, breakout — are dropped, and the text they carry is kept.
subsup becomes <sub> or <sup>.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
-
net10.0
- No dependencies.
-
net11.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.