A Go library and CLI for bidirectional conversion between Jira Atlassian Document Format (ADF) and GitHub Flavored Markdown (GFM).
The project focuses on semantic, AI-friendly output while preserving round-trip metadata where possible.
- Bidirectional conversion APIs:
converterpackage: ADF JSON -> Markdownmdconverterpackage: Markdown -> ADF JSON
- Granular, JSON-serializable configuration for formatting, detection, unknown handling, and extensions.
- Structured conversion results with warnings (
Result{Markdown|ADF, Warnings}). - Runtime link/media hooks in both directions with context, source-path support, and strict/best-effort unresolved behavior.
- Registry-based Extension Hook system to serialize specific ADF extensions as custom Markdown.
- Pandoc-flavored Markdown support for maximum fidelity (bracketed spans, fenced divs, grid tables).
- CLI presets (
balanced,strict,readable,lossy,pandoc) with directional mapping.
See docs/features.md for detailed node/mark coverage and parsing rules.
go get github.com/rgonek/jira-adf-convertergo install github.com/rgonek/jira-adf-converter/cmd/jac@latestForward conversion (ADF JSON -> Markdown):
jac input.adf.jsonReverse conversion (Markdown -> ADF JSON):
jac --reverse input.mdReverse mode prints pretty-formatted ADF JSON.
Common options:
--preset=balanced|strict|readable|lossy|pandoc--allow-html(compatibility override; in forward mode it forces HTML-oriented rendering for underline/subsup/hard breaks/expand, and in reverse mode it enables broad HTML mention/expand detection)--strict(compatibility override; in forward mode it enforces unknown-node/mark errors, and in reverse mode it applies strict detection defaults)--fail-on-warning(exit with code2when conversion succeeds but emits warnings)
Example:
jac --preset=readable input.adf.json > output.md
jac --reverse --preset=strict input.md > output.adf.jsonPreset precedence in CLI is deterministic: preset first, then compatibility overrides (--allow-html, --strict).
CLI output contract is stream-safe for scripting:
- Converted payload is written to
stdout. - Warnings are written to
stderrwithtype,node, optionalcontext, and message fields. - With
--fail-on-warning, output is still emitted, but the process exits non-zero when warnings exist.
package main
import (
"fmt"
"os"
"github.com/rgonek/jira-adf-converter/converter"
)
func main() {
input, err := os.ReadFile("input.adf.json")
if err != nil {
panic(err)
}
conv, err := converter.New(converter.Config{
MentionStyle: converter.MentionLink,
PanelStyle: converter.PanelGitHub,
})
if err != nil {
panic(err)
}
result, err := conv.Convert(input)
if err != nil {
panic(err)
}
fmt.Print(result.Markdown)
for _, w := range result.Warnings {
fmt.Printf("warning: %s (%s): %s\n", w.Type, w.NodeType, w.Message)
}
}package main
import (
"fmt"
"github.com/rgonek/jira-adf-converter/mdconverter"
)
func main() {
conv, err := mdconverter.New(mdconverter.ReverseConfig{
MentionDetection: mdconverter.MentionDetectLink,
PanelDetection: mdconverter.PanelDetectGitHub,
})
if err != nil {
panic(err)
}
result, err := conv.Convert("[Page](https://example.com)")
if err != nil {
panic(err)
}
fmt.Println(string(result.ADF))
}Use ConvertWithContext when you need cancellation/timeouts, deterministic relative-path resolution, or custom mapping for links, media, and extensions.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
adfJSON := []byte(`{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Page","marks":[{"type":"link","attrs":{"href":"https://confluence.example/wiki/pages/123"}}]}]}]}`)
cfg := converter.Config{
ResolutionMode: converter.ResolutionBestEffort,
LinkHook: func(ctx context.Context, in converter.LinkRenderInput) (converter.LinkRenderOutput, error) {
if strings.HasPrefix(in.Href, "https://confluence.example/wiki/pages/") {
return converter.LinkRenderOutput{Href: "../pages/123.md", Handled: true}, nil
}
return converter.LinkRenderOutput{Handled: false}, nil
},
ExtensionHandlers: map[string]converter.ExtensionHandler{
"plantumlcloud": &MyPlantUMLHandler{},
},
}
conv, _ := converter.New(cfg)
result, err := conv.ConvertWithContext(ctx, adfJSON, converter.ConvertOptions{
SourcePath: "docs/spec.adf.json",
})
_ = result
_ = errReverse hooks use the same model (mdconverter.LinkHook / mdconverter.MediaHook / mdconverter.ExtensionHandler) and receive ConvertOptions{SourcePath: ...} for consistent relative reference mapping.
best_effort(default): if a hook returnsErrUnresolved, conversion continues with fallback behavior and adds a warning.strict:ErrUnresolvedfails conversion.
- Forward link hook (
LinkRenderOutput): handled output needsHrefunlessTextOnly=true. - Forward media hook (
MediaRenderOutput): handled output needs non-emptyMarkdown. - Reverse link hook (
LinkParseOutput): handled output needs non-emptyDestination;ForceLinkandForceCardcannot both be true. - Reverse media hook (
MediaParseOutput): handled output requires supportedMediaType(imageorfile) and exactly one ofIDorURL.
| Field | Default |
|---|---|
UnderlineStyle |
bold |
SubSupStyle |
html |
MentionStyle |
link |
PanelStyle |
github |
HardBreakStyle |
backslash |
ExpandStyle |
html |
LayoutSectionStyle |
standard |
InlineCardStyle |
link |
TableMode |
auto |
Extensions.Default |
json |
UnknownNodes |
placeholder |
UnknownMarks |
skip |
ResolutionMode |
best_effort |
| Field | Default |
|---|---|
MentionDetection |
link |
EmojiDetection |
shortcode |
StatusDetection |
bracket |
DateDetection |
iso |
PanelDetection |
github |
LayoutSectionDetection |
html |
ExpandDetection |
html |
DecisionDetection |
emoji |
ResolutionMode |
best_effort |
jac supports balanced, strict, readable, lossy, and pandoc presets in both directions.
balanced: library defaults (recommended for most workflows).strict: stronger fidelity/parsing constraints.readable: favors cleaner human-facing markdown and lenient reverse patterns.lossy: favors compact output over metadata preservation.pandoc: produces Pandoc-flavored Markdown with bracketed spans and fenced divs for maximum metadata preservation.
- Converter instances are safe for concurrent
Convert/ConvertWithContextcalls. - Hook closures are caller-owned and must protect shared mutable state.
# Run unit and golden tests
make test
# Verify formatting without rewriting files
make fmt-check
# Run vet-based linting
make lint
# Optional stronger analysis (requires installed tools)
make staticcheck
make vuln-check
# Ensure module metadata is normalized
make tidy-check
# Run the standard local quality gate
make check- CI runs a multi-platform/multi-version test matrix (
ubuntu-latest,windows-latest; Go1.24.xand1.25.x). - Quality gates enforce
gofmtcleanliness,go mod tidydrift checks,go vet,staticcheck,govulncheck, and shuffled test runs. - A dedicated Linux race job runs
go test -racewithCGO_ENABLED=1and an explicit C toolchain install. - A scheduled
Securityworkflow runs weekly govulncheck and CodeQL analysis. - Dependabot is configured for Go modules and GitHub Actions updates.
- Detailed feature matrix and syntax mapping:
docs/features.md - Development roadmap plans:
agents/plans/
- License:
LICENSE(MIT) - Security reporting policy:
SECURITY.md - Contribution guide:
CONTRIBUTING.md - Changelog:
CHANGELOG.md - Release runbook:
RELEASING.md