adf

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package adf represents an Atlassian Document Format (ADF) page pulled from Confluence, renders it to Markdown, and back-ports edits for push.

The input is the cached wrapper JSON produced by a page pull, of the shape

{"name":…,"id":…,"title":…,"version":…,"space_id":…,"adf":{ADF doc}}

NewADF parses it and ADF.MarshallMarkdown renders the whole document, YAML frontmatter included. ADF.Put and ADF.Merge3 are the push lens: they rebuild ADF from an edited Markdown body while preserving structure the Markdown cannot express.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrMergeConflict = errors.New("push: merge conflict")

ErrMergeConflict is wrapped by every conflict ADF.Merge3 returns, so a caller can distinguish a genuine three-way-merge conflict from a lens-law or other push failure with errors.Is.

Functions

func NewLocalID

func NewLocalID() (string, error)

NewLocalID mints a fresh ADF node localId — six random bytes as twelve hex digits, matching the localId shape Confluence assigns — for a synthesized media node whose attachment has just been uploaded.

Types

type ADF

type ADF struct {
	// Name is the page's destination name, relative to the work directory and
	// ending in ".md". It is rendered as the page_path frontmatter field, the
	// path a user passes to "cfsync push".
	Name string `json:"name"`

	// ID is the numeric Confluence page identifier.
	ID string `json:"id"`

	// Title is the page title as stored in Confluence.
	Title string `json:"title"`

	// Version is the Confluence page version number.
	Version int `json:"version"`

	// SpaceID is the numeric identifier of the space the page belongs to.
	SpaceID string `json:"space_id"`

	// SpaceKey is the key of the space the page belongs to. It is set only for
	// a page pulled through a configured space and rendered as the space_key
	// frontmatter field; it is empty, and omitted from the frontmatter, for a
	// page pulled through pages: or folders:.
	SpaceKey string `json:"space_key,omitempty"`

	// Domain is the Confluence Site host the page was pulled from, such as
	// "example.atlassian.net". It is rendered as the cf_domain frontmatter
	// field and omitted when empty.
	Domain string `json:"cf_domain,omitempty"`

	// Doc is the root of the ADF document tree, a node of type "doc".
	Doc Node `json:"adf"`
}

ADF is a Confluence page in Atlassian Document Format together with the wrapper metadata needed to render its Markdown frontmatter.

func NewADF

func NewADF(data []byte) (*ADF, error)

NewADF parses the cached wrapper JSON into an ADF value.

func (*ADF) FileMedia

func (adf *ADF) FileMedia() []MediaRef

FileMedia returns a MediaRef for every uploaded-file media node in the document, in document order, including those inside a mediaGroup and inline mediaInline file references. External media is omitted (it carries its own URL and is not downloaded). A node with neither a localId nor a fileId cannot be anchored to an asset and is omitted; one lacking only a localId falls back to its fileId as the anchor key (see [Node.mediaAssetKey]), so a file Confluence left without a localId is still downloaded and rendered rather than dropped to a placeholder.

func (*ADF) MarshallMarkdown

func (adf *ADF) MarshallMarkdown(assets map[string]string) ([]byte, error)

MarshallMarkdown renders the document as Markdown: YAML frontmatter followed by the rendered body, ending with a single newline. It errors when the root node is not an ADF "doc".

The assets map links each uploaded-file media node to its downloaded image: it maps a media node's localId (see MediaRef.LocalID) to the image path, relative to the Markdown file. A media node present in the map renders as a Markdown image and contributes a page_images frontmatter entry; one absent from it, including every node when assets is nil, renders as a read-only anchor directive (see [Node.renderAnchor]).

Example
package main

import (
	"fmt"

	"github.com/ctx42/cfsync/pkg/adf"
)

func main() {
	data := `{
	   "name": "demo.md",
	   "title": "Demo",
	   "id": "1",
	   "version": 1,
	   "space_id": "2",
	   "adf": {
	      "type": "doc",
	      "content": [
	         {
	            "type": "heading",
	            "attrs": { "level": 1 },
	            "content": [ { "type": "text", "text": "Hello" } ]
	         },
	         {
	            "type": "paragraph",
	            "content": [
	               {
	                  "type": "text",
	                  "text": "Bold",
	                  "marks": [ { "type": "strong" } ]
	               },
	               { "type": "text", "text": " and plain." }
	            ]
	         }
	      ]
	   }
	}`

	doc, err := adf.NewADF([]byte(data))
	if err != nil {
		panic(err)
	}
	md, err := doc.MarshallMarkdown(nil)
	if err != nil {
		panic(err)
	}
	fmt.Print(string(md))
}
Output:
---
title: "Demo"
page_path: "demo.md"
page_id: "1"
page_version: 1
space_id: "2"
---

# Hello

**Bold** and plain.
func (adf *ADF) MarshallMarkdownLinks(
	assets map[string]string,
	links Links,
) ([]byte, error)

MarshallMarkdownLinks renders the document as ADF.MarshallMarkdown does, rewriting each link to a pulled Confluence page into its local Markdown link via links (see Links). A nil links renders identically to MarshallMarkdown.

func (*ADF) MarshallMarkdownMapped

func (adf *ADF) MarshallMarkdownMapped(
	assets map[string]string,
) ([]byte, *SourceMap, error)

MarshallMarkdownMapped renders the document exactly as ADF.MarshallMarkdown does and additionally returns a SourceMap describing where each top-level block landed in the output and which ADF node produced it. The byte slice it returns is identical to what MarshallMarkdown returns for the same input; the map is the side table push needs to back-port edits without writing any anchor into the Markdown. See Origin for the role it plays.

func (*ADF) Merge3

func (adf *ADF) Merge3(
	remote *ADF,
	body string,
	assets map[string]string,
) (string, error)

Merge3 performs a block-level three-way merge. The receiver is the common baseline (the cached version the local Markdown was edited from); remote is the current live document; body is the edited local Markdown. A push carries that baseline version as the common ancestor when the remote has moved: Merge3 rebases the local edits onto remote so non-overlapping edits combine and only a block edited on both sides is a conflict. It returns a merged body to Put against remote: a block changed on only one side takes that side's version, a block changed the same way on both sides takes it once, and a block changed incompatibly — or a spot where both sides inserted — is a conflict. The merge is deterministic and never mutates a document; correctness of the resulting ADF is still gated by the lens laws when the merged body is Put.

Example
package main

import (
	"fmt"

	"github.com/ctx42/cfsync/pkg/adf"
)

func main() {
	// tpl builds a two-paragraph document with stable localIds so blocks match
	// across the baseline and the remote.
	tpl := func(first, second string) *adf.ADF {
		data := `{ "adf": { "type": "doc", "content": [
		   { "type": "paragraph", "attrs": { "localId": "p1" },
		     "content": [ { "type": "text", "text": "` + first + `" } ] },
		   { "type": "paragraph", "attrs": { "localId": "p2" },
		     "content": [ { "type": "text", "text": "` + second + `" } ] } ] } }`
		doc, err := adf.NewADF([]byte(data))
		if err != nil {
			panic(err)
		}
		return doc
	}

	base := tpl("alpha", "beta")          // the cached baseline
	remote := tpl("alpha", "beta remote") // the live page changed the second

	// The local edit changed only the first paragraph. Merge3 rebases it onto
	// the remote, keeping the remote's change to the second.
	merged, err := base.Merge3(remote, "alpha local\n\nbeta", nil)
	if err != nil {
		panic(err)
	}
	fmt.Println(merged)
}
Output:
alpha local

beta remote
func (adf *ADF) Merge3Links(
	remote *ADF,
	body string,
	assets map[string]string,
	links Links,
) (string, error)

Merge3Links is ADF.Merge3 with a Links so the baseline and remote renders use the same local-link rewriting as the edited body; a nil links behaves exactly like Merge3.

func (*ADF) Put

func (adf *ADF) Put(
	body string,
	mentions map[string]string,
	assets map[string]string,
	images []NewImage,
) (*ADF, error)

Put back-ports the edits expressed in an edited Markdown body into the cached document and returns the new document, ready to push. It is the lens put of the push design: the result is what the edited Markdown expresses combined with everything else copied untouched from the cached ADF, so nothing the Markdown cannot express (localId, panel types, macros, table structure) is lost.

body is the edited Markdown body only, with the frontmatter already stripped; mentions is the display-name→account-id map from that frontmatter, used to resolve [[@name]] mentions; assets is the same media map used to render the page on pull, so baseline blocks match. The result is a fresh document; the receiver is not modified.

Put applies in-place edits and structural inserts/deletes that the Markdown can express — paragraphs, headings, lists, panels, tables, and images among them — and rejects a change that would be lossy rather than guessing. A modified block whose original inline does not round-trip is refused.

Before returning, Put verifies both lens laws: an unchanged block re-renders byte-identically to the cached render (GetPut), and every block re-renders to the user's edit (PutGet). A violation is returned as an error, never pushed.

Example
package main

import (
	"fmt"

	"github.com/ctx42/cfsync/pkg/adf"
)

func main() {
	// The document pulled from Confluence and cached.
	data := `{
	   "name": "demo.md", "title": "Demo", "id": "1", "version": 1, "space_id": "2",
	   "adf": { "type": "doc", "content": [
	      { "type": "paragraph", "attrs": { "localId": "p" },
	        "content": [ { "type": "text", "text": "The original text." } ] } ] }
	}`
	doc, err := adf.NewADF([]byte(data))
	if err != nil {
		panic(err)
	}

	// The user edited the rendered Markdown body; back-port that edit into the
	// cached document, ready to push.
	edited, err := doc.Put("The edited text.", nil, nil, nil)
	if err != nil {
		panic(err)
	}

	md, err := edited.MarshallMarkdown(nil)
	if err != nil {
		panic(err)
	}
	fmt.Print(string(md))
}
Output:
---
title: "Demo"
page_path: "demo.md"
page_id: "1"
page_version: 1
space_id: "2"
---

The edited text.
func (adf *ADF) PutLinks(
	body string,
	mentions map[string]string,
	assets map[string]string,
	images []NewImage,
	links Links,
) (*ADF, error)

PutLinks is ADF.Put with a Links that maps local Markdown links in the edited body back to the Confluence hrefs to push, and renders the baseline with the same mapping so an unedited cross-linked block is not seen as a change. A nil links behaves exactly like Put.

type Links interface {
	// ToLocal maps a Confluence href to a local Markdown link target and the
	// label to show for it. It returns ok false to leave the link unchanged.
	// The label is used only when an inlineCard, which carries no text of its
	// own, is rewritten into a "[label](target)" link; a text link keeps its
	// existing label.
	ToLocal(href string) (target, label string, ok bool)

	// ToRemote maps a local Markdown link target back to the Confluence href to
	// push. It returns ok false to leave the target unchanged.
	ToRemote(target string) (href string, ok bool)
}

Links translates page links between a Confluence document and its local Markdown rendering, for one document at a known location. It is supplied by the caller so the adf package stays ignorant of how pages map to local files.

On render (see ADF.MarshallMarkdownLinks), ToLocal turns a Confluence href into a local Markdown link target; on reconstruct (see ADF.PutLinks), ToRemote turns a local target back into the Confluence href to push. The two are inverses for a link that survives a pull/push round trip, so an unedited document re-renders unchanged. A nil Links leaves every link untouched.

type Mark

type Mark struct {
	// Type is the mark type.
	Type string `json:"type"`

	// Attrs holds the mark's type-specific attributes, such as a link "href".
	Attrs map[string]any `json:"attrs,omitempty"`
}

Mark is an inline formatting mark applied to a text node, such as "strong", "em" or "link".

type MediaRef

type MediaRef struct {
	// LocalID is the media node's ADF localId, the stable per-node anchor used
	// as the frontmatter key and the assets-map key in [ADF.MarshallMarkdown].
	LocalID string

	// FileID is the media node's attrs.id, equal to the Confluence attachment
	// fileId; it is the key that matches a node to a downloadable attachment.
	FileID string

	// Alt is the media node's attrs.alt, the original file name.
	Alt string
}

MediaRef identifies an uploaded-file image referenced by the document, the information a caller needs to fetch it and link the download back to its ADF node.

type NewImage

type NewImage struct {
	Path       string // the ![](path) target as written in the Markdown
	Alt        string // the ![alt] text
	FileID     string // attachment fileId → media attrs.id
	LocalID    string // minted node localId (see [NewLocalID])
	Collection string // media attrs.collection, e.g. "contentId-<pageID>"
}

NewImage describes a user-added local image to splice into the document on push: the Markdown path as it appears in the edited body, the alt text, and the attributes of the Confluence attachment it was uploaded as. The lens turns each inserted ![alt](Path) block whose target is a NewImage into a mediaSingle+media node; any other inserted image is rejected, as it has no attachment to point at.

type Node

type Node struct {
	// Type is the ADF node type, such as "paragraph", "text" or "table".
	Type string `json:"type"`

	// Content holds the node's children, empty for leaf nodes.
	Content []Node `json:"content,omitempty"`

	// Text is the literal text of a "text" node, empty otherwise.
	Text string `json:"text,omitempty"`

	// Marks are the inline formatting marks applied to a "text" node.
	Marks []Mark `json:"marks,omitempty"`

	// Attrs holds the node's type-specific attributes.
	Attrs map[string]any `json:"attrs,omitempty"`
}

Node is a single node in the ADF document tree. The same struct models block nodes, inline nodes, and text leaves; which fields are populated depends on Node.Type.

type Origin

type Origin struct {
	// NodeIndex is the position, in the document's top-level Content slice, of the
	// source node. Blocks that render to nothing have no origin, so indices are
	// not necessarily contiguous. Put matches edits by content and applies them
	// through this index.
	NodeIndex int

	// Type is the source node's ADF type, such as "paragraph" or "table".
	Type string

	// LocalID is the source node's localId attribute, or "" when it has none.
	// It is recorded for tooling and diagnostics; push matching uses content
	// alignment, not this field.
	LocalID string

	// Span is the block's byte range in the rendered Markdown.
	Span Span
}

Origin links one rendered top-level block back to the ADF node that produced it. It is the invisible anchor that makes push possible: on push, cfsync re-renders the cached ADF, aligns the user's edited Markdown blocks against those origins by normalized content (LCS via [diffBlocks]), and rebuilds the ADF from the cached tree plus the expressed edits using Origin.NodeIndex. No marker is written into the Markdown itself.

type SourceMap

type SourceMap struct {
	// BodyStart is the byte offset at which the rendered body begins, just after
	// the frontmatter and its separating blank line. It equals the length of the
	// output when the document has no rendered body.
	BodyStart int

	// Origins holds one entry per non-empty top-level block, in document order.
	Origins []Origin
}

SourceMap is the ordered origin table for one render: the byte where the body begins and, per non-empty top-level block, the Origin linking it to its source node. Its zero value describes a document with no rendered body.

type Span

type Span struct {
	Start int
	End   int
}

Span is the half-open byte range [Start, End) that a rendered block occupies in the Markdown produced by ADF.MarshallMarkdownMapped. The offsets index the returned byte slice, frontmatter included.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL