Documentation
¶
Overview ¶
Package adf converts between Markdown and the Atlassian Document Format (ADF), the JSON format Jira Cloud and Confluence Cloud use for rich text.
FromMarkdown always produces documents that validate against Atlassian's published ADF JSON schema: Markdown constructs that ADF cannot express in a given place are rewritten into the closest valid form (for example a heading inside a list item becomes a bold paragraph). ToMarkdown accepts any ADF document and falls back to the text content of node types it does not know.
Both directions are safe for untrusted input: they never panic, and nesting deeper than the configured limit is rejected with ErrTooDeep.
Example (ExtendedSyntax) ¶
package main
import (
"fmt"
adf "github.com/nyactl/go-adf"
)
func main() {
md := "<details>\n<summary>Details</summary>\n\nSee <card:https://example.atlassian.net/browse/PROJ-1>, {color:#006644}done{/color}.\n\n</details>"
doc, _ := adf.FromMarkdown(md, adf.WithExtendedSyntax())
fmt.Println(string(doc))
back, _ := adf.ToMarkdown(doc, adf.WithExtendedSyntax())
fmt.Println(back == md)
}
Output: {"type":"doc","version":1,"content":[{"type":"expand","attrs":{"title":"Details"},"content":[{"type":"paragraph","content":[{"type":"text","text":"See "},{"type":"inlineCard","attrs":{"url":"https://example.atlassian.net/browse/PROJ-1"}},{"type":"text","text":", "},{"type":"text","text":"done","marks":[{"type":"textColor","attrs":{"color":"#006644"}}]},{"type":"text","text":"."}]}]}]} true
Example (Mentions) ¶
Mentions are written as @[label]. The hooks decide how people are named and resolved, for example to keep real names out of the Markdown.
package main
import (
"fmt"
"strings"
adf "github.com/nyactl/go-adf"
)
func main() {
people := map[string]string{"Person A": "5b10ac8d82e05b22cc7d4ef5"}
resolve := adf.WithMentionResolver(func(label string) (string, string, bool) {
id, ok := people[label]
return id, label, ok
})
doc, err := adf.FromMarkdown("Thanks @[Person A]!", resolve)
if err != nil {
panic(err)
}
md, err := adf.ToMarkdown(doc, adf.WithMentionText(func(accountID, text string) string {
return strings.TrimPrefix(text, "@")
}))
if err != nil {
panic(err)
}
fmt.Println(md)
}
Output: Thanks @[Person A]!
Example (Passthrough) ¶
package main
import (
"encoding/json"
"fmt"
"strings"
adf "github.com/nyactl/go-adf"
)
func main() {
page := `{"type":"doc","version":1,"content":[
{"type":"paragraph","content":[{"type":"text","text":"Contents:"}]},
{"type":"extension","attrs":{"extensionType":"com.atlassian.confluence.macro.core","extensionKey":"toc"}}]}`
md, _ := adf.ToMarkdown(json.RawMessage(page), adf.WithPassthrough())
fmt.Println(md)
edited := strings.Replace(md, "Contents:", "On this page:", 1)
doc, _ := adf.FromMarkdown(edited, adf.WithPassthrough())
fmt.Println(string(doc))
}
Output: Contents: ```adf {"type":"extension","attrs":{"extensionKey":"toc","extensionType":"com.atlassian.confluence.macro.core"}} ``` {"type":"doc","version":1,"content":[{"type":"paragraph","content":[{"type":"text","text":"On this page:"}]},{"type":"extension","attrs":{"extensionKey":"toc","extensionType":"com.atlassian.confluence.macro.core"}}]}
Index ¶
- Constants
- Variables
- func FromMarkdown(markdown string, opts ...Option) (json.RawMessage, error)
- func ToMarkdown(raw json.RawMessage, opts ...Option) (string, error)
- type Mark
- type MediaError
- type MentionError
- type Node
- type Option
- func WithExtendedSyntax() Option
- func WithMaxDepth(n int) Option
- func WithMediaResolver(f func(name string) (media Node, ok bool)) Option
- func WithMentionResolver(f func(label string) (accountID, text string, ok bool)) Option
- func WithMentionText(f func(accountID, text string) string) Option
- func WithPassthrough() Option
- func WithRawNodes(h RawNodes) Option
- type RawNodes
Examples ¶
Constants ¶
const DefaultMaxDepth = 100
DefaultMaxDepth is the nesting limit used unless WithMaxDepth sets one.
Variables ¶
var ErrRawNode = errors.New("adf: invalid adf block")
ErrRawNode is returned by FromMarkdown, with WithPassthrough, for an adf block or span that does not hold one valid node for its place.
var ErrSyntax = errors.New("adf: invalid extended syntax")
ErrSyntax is returned by FromMarkdown, with WithExtendedSyntax, for a columns block whose column blocks are not as described there.
var ErrTooDeep = errors.New("adf: document is nested too deeply")
ErrTooDeep is returned for documents nested deeper than the limit.
Functions ¶
func FromMarkdown ¶
func FromMarkdown(markdown string, opts ...Option) (json.RawMessage, error)
FromMarkdown converts CommonMark with the GitHub extensions for tables, strikethrough and task list checkboxes to an ADF document.
Supported: paragraphs, headings, bullet and ordered lists, fenced and indented code, block quotes, thematic breaks, tables, strong, emphasis, strikethrough, inline code, links, autolinks and, with WithMentionResolver, @[label] mentions. Images become links. Raw HTML is kept as text.
Example ¶
package main
import (
"fmt"
adf "github.com/nyactl/go-adf"
)
func main() {
doc, err := adf.FromMarkdown("Fixed in **v2.1**, see [notes](https://example.com/notes).")
if err != nil {
panic(err)
}
fmt.Println(string(doc))
}
Output: {"type":"doc","version":1,"content":[{"type":"paragraph","content":[{"type":"text","text":"Fixed in "},{"type":"text","text":"v2.1","marks":[{"type":"strong"}]},{"type":"text","text":", see "},{"type":"text","text":"notes","marks":[{"type":"link","attrs":{"href":"https://example.com/notes"}}]},{"type":"text","text":"."}]}]}
func ToMarkdown ¶
func ToMarkdown(raw json.RawMessage, opts ...Option) (string, error)
ToMarkdown renders an ADF document as Markdown. Empty or null input yields "", and a JSON string (which some Jira fields still hold instead of a document) is returned as is. Unknown node types fall back to their text content.
Example ¶
package main
import (
"encoding/json"
"fmt"
adf "github.com/nyactl/go-adf"
)
func main() {
doc := json.RawMessage(`{"type":"doc","version":1,"content":[
{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"Steps"}]},
{"type":"orderedList","content":[
{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Run "},{"type":"text","text":"make test","marks":[{"type":"code"}]}]}]},
{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Ship it"}]}]}]}]}`)
md, err := adf.ToMarkdown(doc)
if err != nil {
panic(err)
}
fmt.Println(md)
}
Output: ## Steps 1. Run `make test` 2. Ship it
Types ¶
type MediaError ¶ added in v0.2.0
type MediaError struct {
Name string
}
MediaError is returned by FromMarkdown when an image refers to an attachment that the media resolver does not know, or when there is no resolver.
func (*MediaError) Error ¶ added in v0.2.0
func (e *MediaError) Error() string
type MentionError ¶
type MentionError struct {
Label string
}
MentionError is returned by FromMarkdown when the mention resolver does not know a @[label].
func (*MentionError) Error ¶
func (e *MentionError) Error() string
type Node ¶
type Node struct {
Type string `json:"type"`
Version int `json:"version,omitempty"`
Attrs map[string]any `json:"attrs,omitempty"`
Content []Node `json:"content,omitempty"`
Text string `json:"text,omitempty"`
Marks []Mark `json:"marks,omitempty"`
}
Node is an ADF node.
type Option ¶
type Option func(*config)
Option configures a conversion. Options that do not apply to a direction are ignored.
func WithExtendedSyntax ¶ added in v0.2.0
func WithExtendedSyntax() Option
WithExtendedSyntax adds Markdown forms for elements of the Confluence and Jira editors that CommonMark lacks. Both directions use them:
- expand: <details>, an optional <summary>Title</summary> line and the body, closed by </details>; inside an expand it is a nestedExpand. The editor's width setting is kept as data-breakout="wide:760".
- column layout: a fenced block with the info string columns holding one fenced block per column with the info string column and its width in percent, e.g. "column 50"; the width setting as in breakout=wide:760.
- smart links: <card:URL> inline, <card-block:URL> and <card-embed:URL> on a line of their own.
- text color and highlight: {color:#RRGGBB}text{/color} and {highlight:#RRGGBB}text{/highlight}.
- images on a line of their own:  for external images, and  for attachments, with WithMediaResolver. The title can hold the layout and the width: "center 600px" or "wide 50%".
Text that only looks like one of these forms is escaped, and code in the language columns loses that language, which would make it a layout (with WithPassthrough it is kept). A columns block with malformed column blocks makes FromMarkdown fail with ErrSyntax; one without any column block is code, and an image with a title other than these options stays a link.
func WithMaxDepth ¶
WithMaxDepth sets the nesting limit for both directions.
func WithMediaResolver ¶ added in v0.2.0
WithMediaResolver lets WithExtendedSyntax write images stored as attachments. ToMarkdown names such an image by its alt text, usually the file name, and FromMarkdown asks the resolver for the media node of that name; when it reports false, FromMarkdown fails with a *MediaError. Without a resolver, ToMarkdown keeps writing attachments as before.
func WithMentionResolver ¶
WithMentionResolver makes FromMarkdown turn @[label] into mention nodes. The resolver returns the account ID and the display name to store; when it reports false, FromMarkdown fails with a *MentionError. Without a resolver, @[label] stays plain text.
func WithMentionText ¶
WithMentionText sets how ToMarkdown names a mentioned person. It receives the account ID and the text stored with the mention (usually "@Name") and returns the label written as @[label]. By default the stored text is used.
func WithPassthrough ¶ added in v0.2.0
func WithPassthrough() Option
WithPassthrough keeps what Markdown cannot express, so that FromMarkdown(ToMarkdown(doc)) keeps it unchanged. ToMarkdown writes such a block node, for example an app macro (extension, bodiedExtension), a panel or a table with merged cells, as a fenced code block with the info string adf holding the node's JSON. Such an inline node, for example a status or an inlineExtension, and text with formatting other than links, emphasis, strikethrough and code, become inline code holding adf: and the JSON. FromMarkdown turns both forms back into the node, after checking that it is one JSON object with a type that fits its place, and fails with ErrRawNode otherwise. Attributes of nodes that do have a Markdown form, such as table widths, are still not kept.
Callers that take Markdown from others decide which of these nodes they accept: the option lets the author write any node.
func WithRawNodes ¶ added in v0.2.0
WithRawNodes turns on WithPassthrough and passes every node it writes or reads as JSON through h. Inline code that a span of nodes takes along is not passed.
type RawNodes ¶ added in v0.2.0
type RawNodes interface {
// Write returns the node to write as JSON in place of n.
Write(n Node) Node
// Read returns the node to use for n, read from an adf block or, when
// inline is set, from adf inline code. An error makes FromMarkdown fail
// with it.
Read(n Node, inline bool) (Node, error)
}
RawNodes decides, with WithRawNodes, what stands in the Markdown for the nodes WithPassthrough writes as JSON. An application can write a placeholder instead of a node whose content it does not want to show, and put the node back when reading, or refuse nodes it did not write.