Files
mark/renderer/gh_alerts_blockquote.go
Noam Asor 1c1eeb84fb feat: add GitHub Alerts transformer and renderers
Co-Authored-By: Manuel Rüger <manuel@rueg.eu>
2026-03-31 20:54:07 +02:00

151 lines
4.7 KiB
Go

package renderer
import (
"fmt"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceGHAlertsBlockQuoteRenderer struct {
html.Config
LevelMap BlockQuoteLevelMap
BlockQuoteNode ast.Node
}
// NewConfluenceGHAlertsBlockQuoteRenderer creates a new instance of the renderer for GitHub Alerts
func NewConfluenceGHAlertsBlockQuoteRenderer(opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceGHAlertsBlockQuoteRenderer{
Config: html.NewConfig(),
LevelMap: nil,
BlockQuoteNode: nil,
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs
func (r *ConfluenceGHAlertsBlockQuoteRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindBlockquote, r.renderBlockQuote)
}
// Define GitHub Alert to Confluence macro mapping
func (r *ConfluenceGHAlertsBlockQuoteRenderer) getConfluenceMacroName(alertType string) string {
switch alertType {
case "note":
return "info"
case "tip":
return "tip"
case "important":
return "info"
case "warning":
return "note"
case "caution":
return "warning"
default:
return "info"
}
}
func (r *ConfluenceGHAlertsBlockQuoteRenderer) renderBlockQuote(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if r.LevelMap == nil {
r.LevelMap = GenerateBlockQuoteLevel(node)
}
// Check if this blockquote has been transformed by the GHAlerts transformer
if alertTypeBytes, hasAttribute := node.Attribute([]byte("gh-alert-type")); hasAttribute && alertTypeBytes != nil {
if alertTypeStr, ok := alertTypeBytes.([]byte); ok {
return r.renderGHAlert(writer, source, node, entering, string(alertTypeStr))
}
}
// Fall back to legacy blockquote rendering for non-GitHub Alert blockquotes
return r.renderLegacyBlockQuote(writer, source, node, entering)
}
func (r *ConfluenceGHAlertsBlockQuoteRenderer) renderGHAlert(writer util.BufWriter, source []byte, node ast.Node, entering bool, alertType string) (ast.WalkStatus, error) {
quoteLevel := r.LevelMap.Level(node)
if quoteLevel == 0 && entering {
r.BlockQuoteNode = node
macroName := r.getConfluenceMacroName(alertType)
prefix := fmt.Sprintf("<ac:structured-macro ac:name=\"%s\"><ac:parameter ac:name=\"icon\">true</ac:parameter><ac:rich-text-body>\n", macroName)
if _, err := writer.Write([]byte(prefix)); err != nil {
return ast.WalkStop, err
}
return ast.WalkContinue, nil
}
if quoteLevel == 0 && !entering && node == r.BlockQuoteNode {
suffix := "</ac:rich-text-body></ac:structured-macro>\n"
if _, err := writer.Write([]byte(suffix)); err != nil {
return ast.WalkStop, err
}
return ast.WalkContinue, nil
}
// For nested blockquotes or continuing the content, use default rendering
if quoteLevel > 0 {
if entering {
if _, err := writer.WriteString("<blockquote>\n"); err != nil {
return ast.WalkStop, err
}
} else {
if _, err := writer.WriteString("</blockquote>\n"); err != nil {
return ast.WalkStop, err
}
}
} else if quoteLevel == 0 && alertType == "" {
// This handles the fallback case for non-alert blockquotes if called accidentally
if entering {
if _, err := writer.WriteString("<blockquote>\n"); err != nil {
return ast.WalkStop, err
}
} else {
if _, err := writer.WriteString("</blockquote>\n"); err != nil {
return ast.WalkStop, err
}
}
}
return ast.WalkContinue, nil
}
func (r *ConfluenceGHAlertsBlockQuoteRenderer) renderLegacyBlockQuote(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
// Legacy blockquote handling (same as original ParseBlockQuoteType logic)
quoteType := ParseBlockQuoteType(node, source)
quoteLevel := r.LevelMap.Level(node)
if quoteLevel == 0 && entering && quoteType != None {
r.BlockQuoteNode = node
prefix := fmt.Sprintf("<ac:structured-macro ac:name=\"%s\"><ac:parameter ac:name=\"icon\">true</ac:parameter><ac:rich-text-body>\n", quoteType)
if _, err := writer.Write([]byte(prefix)); err != nil {
return ast.WalkStop, err
}
return ast.WalkContinue, nil
}
if quoteLevel == 0 && !entering && node == r.BlockQuoteNode {
suffix := "</ac:rich-text-body></ac:structured-macro>\n"
if _, err := writer.Write([]byte(suffix)); err != nil {
return ast.WalkStop, err
}
return ast.WalkContinue, nil
}
// For nested blockquotes or regular blockquotes (at root level with no macro type)
if quoteLevel > 0 || (quoteLevel == 0 && quoteType == None) {
if entering {
if _, err := writer.WriteString("<blockquote>\n"); err != nil {
return ast.WalkStop, err
}
} else {
if _, err := writer.WriteString("</blockquote>\n"); err != nil {
return ast.WalkStop, err
}
}
}
return ast.WalkContinue, nil
}