package main
import (
"encoding/json"
"fmt"
"os"
"github.com/spf13/cobra"
"go.bigb.es/confluence-md-utilities/api"
"go.bigb.es/confluence-md-utilities/converter"
)
var (
pullOutput string
pullRaw bool
pullWithComments bool
)
var pullCmd = &cobra.Command{
Use: "pull <confluence-url>",
Short: "Pull a page from Confluence and convert to Markdown",
Long: `Fetch a Confluence page by URL, extract its storage format body,
and convert it to Markdown.
Supported URL formats:
https://confluence.example.com/pages/viewpage.action?pageId=12345
https://confluence.example.com/display/SPACE/Page+Title
Use --raw to get the Confluence storage XML without converting to Markdown.
Authentication via --token flag or CONFLUENCE_TOKEN environment variable.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
token := resolveToken()
if token == "" {
return fmt.Errorf("Confluence token required: use --token flag or set CONFLUENCE_TOKEN env var")
}
if pullWithComments && pullOutput == "" {
return fmt.Errorf("--with-comments requires --output")
}
ref, err := api.ParsePageURL(args[0])
if err != nil {
return err
}
client := api.NewClient(ref.BaseURL, token)
page, err := client.GetPage(ref)
if err != nil {
return err
}
xmlBody := page.Body.Storage.Value
fmt.Fprintf(os.Stderr, "Pulled page: %s (id=%s, version=%d)\n", page.Title, page.ID, page.Version.Number)
var result string
if pullRaw {
result = xmlBody
} else {
result, err = converter.ConfluenceToMarkdown(xmlBody)
if err != nil {
return fmt.Errorf("converting to markdown: %w", err)
}
}
if pullOutput != "" {
if err := os.WriteFile(pullOutput, []byte(result), 0644); err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Written to %s\n", pullOutput)
if pullWithComments {
if err := pullSaveComments(client, page, pullOutput); err != nil {
return fmt.Errorf("fetching comments: %w", err)
}
}
return nil
}
fmt.Print(result)
return nil
},
}
type commentSidecar struct {
Page sidecarPage `json:"page"`
InlineComments []*api.Comment `json:"inline_comments"`
PageComments []*api.Comment `json:"page_comments"`
}
type sidecarPage struct {
ID string `json:"id"`
Title string `json:"title"`
Version int `json:"version"`
}
func pullSaveComments(client *api.Client, page *api.ContentResponse, mdPath string) error {
comments, err := client.GetComments(page.ID)
if err != nil {
return err
}
inline, footer := splitComments(comments)
sidecar := commentSidecar{
Page: sidecarPage{
ID: page.ID,
Title: page.Title,
Version: page.Version.Number,
},
InlineComments: inline,
PageComments: footer,
}
data, err := json.MarshalIndent(sidecar, "", " ")
if err != nil {
return err
}
sidecarPath := mdPath + ".comments.json"
if err := os.WriteFile(sidecarPath, data, 0644); err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Saved %d inline + %d page comments to %s\n",
len(inline), len(footer), sidecarPath)
return nil
}
func splitComments(all []*api.Comment) (inline, footer []*api.Comment) {
for _, c := range all {
if c.Location == "inline" {
inline = append(inline, c)
} else {
footer = append(footer, c)
}
}
return
}
func init() {
pullCmd.Flags().StringVarP(&pullOutput, "output", "o", "", "Output file (default: stdout)")
pullCmd.Flags().BoolVar(&pullRaw, "raw", false, "Output raw Confluence storage XML instead of Markdown")
pullCmd.Flags().BoolVar(&pullWithComments, "with-comments", false, "Also save inline + page comments to <output>.comments.json (requires --output)")
rootCmd.AddCommand(pullCmd)
}