package confluence import ( "bytes" "fmt" "html" "strings" "github.com/yuin/goldmark/ast" east "github.com/yuin/goldmark/extension/ast" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/util" ) // Renderer renders goldmark AST nodes into Confluence storage format XML. type Renderer struct { taskIDCounter int inTaskBody bool inlineCommentDepth int pendingTableAttrs string // stored from comment pendingCodeMacroID string // stored from comment } // NewRenderer creates a new Confluence storage format renderer. func NewRenderer() renderer.NodeRenderer { return &Renderer{} } func (r *Renderer) nextTaskID() int { r.taskIDCounter++ return r.taskIDCounter } // RegisterFuncs implements renderer.NodeRenderer. func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { // Block nodes reg.Register(ast.KindDocument, r.renderDocument) reg.Register(ast.KindHeading, r.renderHeading) reg.Register(ast.KindParagraph, r.renderParagraph) reg.Register(ast.KindTextBlock, r.renderTextBlock) reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock) reg.Register(ast.KindCodeBlock, r.renderCodeBlock) reg.Register(ast.KindThematicBreak, r.renderThematicBreak) reg.Register(ast.KindBlockquote, r.renderBlockquote) reg.Register(ast.KindList, r.renderList) reg.Register(ast.KindListItem, r.renderListItem) reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock) // Inline nodes reg.Register(ast.KindText, r.renderText) reg.Register(ast.KindString, r.renderString) reg.Register(ast.KindEmphasis, r.renderEmphasis) reg.Register(ast.KindCodeSpan, r.renderCodeSpan) reg.Register(ast.KindLink, r.renderLink) reg.Register(ast.KindAutoLink, r.renderAutoLink) reg.Register(ast.KindImage, r.renderImage) reg.Register(ast.KindRawHTML, r.renderRawHTML) // GFM extensions reg.Register(east.KindTable, r.renderTable) reg.Register(east.KindTableHeader, r.renderTableHeader) // Note: goldmark GFM has no KindTableBody reg.Register(east.KindTableRow, r.renderTableRow) reg.Register(east.KindTableCell, r.renderTableCell) reg.Register(east.KindStrikethrough, r.renderStrikethrough) reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox) } func (r *Renderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { return ast.WalkContinue, nil } func (r *Renderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.Heading) tag := fmt.Sprintf("h%d", n.Level) if entering { fmt.Fprintf(w, "<%s>", tag) } else { fmt.Fprintf(w, "%s>\n", tag) } return ast.WalkContinue, nil } func (r *Renderer) renderParagraph(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { // Inside task items, don't wrap in
- the task-body handles it if r.inTaskBody { if !entering { w.WriteString("\n") r.inTaskBody = false } return ast.WalkContinue, nil } if entering { w.WriteString("
") } else { w.WriteString("
\n") } return ast.WalkContinue, nil } func (r *Renderer) renderTextBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { if r.inTaskBody { w.WriteString("\n") r.inTaskBody = false } else { w.WriteString("\n") } } return ast.WalkContinue, nil } func (r *Renderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkContinue, nil } n := node.(*ast.FencedCodeBlock) language := "" if n.Info != nil { lang := n.Info.Segment.Value(source) // Take first word only (e.g., "go title=foo" -> "go") if idx := bytes.IndexByte(lang, ' '); idx > 0 { lang = lang[:idx] } language = string(lang) } var buf bytes.Buffer for i := 0; i < n.Lines().Len(); i++ { line := n.Lines().At(i) buf.Write(line.Value(source)) } // Remove trailing newline from code content code := strings.TrimRight(buf.String(), "\n") w.WriteString(CodeMacroWithID(language, code, r.pendingCodeMacroID)) r.pendingCodeMacroID = "" w.WriteString("\n") return ast.WalkSkipChildren, nil } func (r *Renderer) renderCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkContinue, nil } n := node.(*ast.CodeBlock) var buf bytes.Buffer for i := 0; i < n.Lines().Len(); i++ { line := n.Lines().At(i) buf.Write(line.Value(source)) } code := strings.TrimRight(buf.String(), "\n") w.WriteString(CodeMacro("", code)) w.WriteString("\n") return ast.WalkSkipChildren, nil } func (r *Renderer) renderThematicBreak(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if entering { w.WriteString("