From 1c1eeb84fb9d526b09a085338548d26235f4bb39 Mon Sep 17 00:00:00 2001
From: Noam Asor ` tag will be used
@@ -750,7 +773,7 @@ Currently this is not compatible with the automated upload of inline images.
### Render Mermaid Diagram
-Confluence doesn't provide [mermaid.js](https://github.com/mermaid-js/mermaid) support natively. Mark provides a convenient way to enable the feature like [Github does](https://github.blog/2022-02-14-include-diagrams-markdown-files-mermaid/).
+Confluence doesn't provide [mermaid.js](https://github.com/mermaid-js/mermaid) support natively. Mark provides a convenient way to enable the feature like [GitHub does](https://github.blog/2022-02-14-include-diagrams-markdown-files-mermaid/).
As long as you have a code block marked as "mermaid", mark will automatically render it as a PNG image and attach it to the page as a rendered version of the code block.
```mermaid title diagrams_example
diff --git a/markdown/markdown.go b/markdown/markdown.go
index b1d5ae5..9b2fb57 100644
--- a/markdown/markdown.go
+++ b/markdown/markdown.go
@@ -8,6 +8,7 @@ import (
cparser "github.com/kovetskiy/mark/v16/parser"
crenderer "github.com/kovetskiy/mark/v16/renderer"
"github.com/kovetskiy/mark/v16/stdlib"
+ ctransformer "github.com/kovetskiy/mark/v16/transformer"
"github.com/kovetskiy/mark/v16/types"
"github.com/rs/zerolog/log"
mkDocsParser "github.com/stefanfritsch/goldmark-admonitions"
@@ -20,8 +21,9 @@ import (
"github.com/yuin/goldmark/util"
)
-// Renderer renders anchor [Node]s.
-type ConfluenceExtension struct {
+// ConfluenceLegacyExtension is the original goldmark extension without GitHub Alerts support
+// This extension is preserved for backward compatibility and testing purposes
+type ConfluenceLegacyExtension struct {
html.Config
Stdlib *stdlib.Lib
Path string
@@ -29,9 +31,9 @@ type ConfluenceExtension struct {
Attachments []attachment.Attachment
}
-// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
-func NewConfluenceExtension(stdlib *stdlib.Lib, path string, cfg types.MarkConfig) *ConfluenceExtension {
- return &ConfluenceExtension{
+// NewConfluenceLegacyExtension creates a new instance of the legacy ConfluenceRenderer
+func NewConfluenceLegacyExtension(stdlib *stdlib.Lib, path string, cfg types.MarkConfig) *ConfluenceLegacyExtension {
+ return &ConfluenceLegacyExtension{
Config: html.NewConfig(),
Stdlib: stdlib,
Path: path,
@@ -40,14 +42,14 @@ func NewConfluenceExtension(stdlib *stdlib.Lib, path string, cfg types.MarkConfi
}
}
-func (c *ConfluenceExtension) Attach(a attachment.Attachment) {
+func (c *ConfluenceLegacyExtension) Attach(a attachment.Attachment) {
c.Attachments = append(c.Attachments, a)
}
-func (c *ConfluenceExtension) Extend(m goldmark.Markdown) {
+func (c *ConfluenceLegacyExtension) Extend(m goldmark.Markdown) {
m.Renderer().AddOptions(renderer.WithNodeRenderers(
- util.Prioritized(crenderer.NewConfluenceTextRenderer(c.MarkConfig.StripNewlines), 100),
+ util.Prioritized(crenderer.NewConfluenceTextLegacyRenderer(c.MarkConfig.StripNewlines), 100),
util.Prioritized(crenderer.NewConfluenceBlockQuoteRenderer(), 100),
util.Prioritized(crenderer.NewConfluenceCodeBlockRenderer(c.Stdlib, c.Path), 100),
util.Prioritized(crenderer.NewConfluenceFencedCodeBlockRenderer(c.Stdlib, c, c.MarkConfig), 100),
@@ -90,10 +92,10 @@ func (c *ConfluenceExtension) Extend(m goldmark.Markdown) {
))
}
-func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, cfg types.MarkConfig) (string, []attachment.Attachment, error) {
- log.Trace().Msgf("rendering markdown:\n%s", string(markdown))
-
- confluenceExtension := NewConfluenceExtension(stdlib, path, cfg)
+// compileMarkdownWithExtension is a shared helper to eliminate code duplication
+// between different compilation approaches
+func compileMarkdownWithExtension(markdown []byte, ext goldmark.Extender, logMessage string) (string, error) {
+ log.Trace().Msgf(logMessage, string(markdown))
converter := goldmark.New(
goldmark.WithExtensions(
@@ -102,7 +104,7 @@ func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, cfg types
extension.NewTable(
extension.WithTableCellAlignMethod(extension.TableCellAlignStyle),
),
- confluenceExtension,
+ ext,
extension.GFM,
),
goldmark.WithParserOptions(
@@ -119,12 +121,128 @@ func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, cfg types
err := converter.Convert(markdown, &buf, parser.WithContext(ctx))
if err != nil {
- return "", nil, err
+ return "", err
}
html := buf.Bytes()
-
log.Trace().Msgf("rendered markdown to html:\n%s", string(html))
- return string(html), confluenceExtension.Attachments, nil
+ return string(html), nil
+}
+
+// CompileMarkdown compiles markdown to Confluence Storage Format with GitHub Alerts support
+// This is the main function that now uses the enhanced GitHub Alerts transformer by default
+// for superior processing of [!NOTE], [!TIP], [!WARNING], [!CAUTION], [!IMPORTANT] syntax.
+// Note: This is a breaking change from previous versions which rendered these markers literally.
+func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, cfg types.MarkConfig) (string, []attachment.Attachment, error) {
+ // Use the enhanced GitHub Alerts extension for better processing
+ ghAlertsExtension := NewConfluenceExtension(stdlib, path, cfg)
+ html, err := compileMarkdownWithExtension(markdown, ghAlertsExtension, "rendering markdown with GitHub Alerts support:\n%s")
+ return html, ghAlertsExtension.Attachments, err
+}
+
+// CompileMarkdownLegacy compiles markdown using the legacy approach without GitHub Alerts transformer
+// This function is preserved for backward compatibility and testing purposes
+func CompileMarkdownLegacy(markdown []byte, stdlib *stdlib.Lib, path string, cfg types.MarkConfig) (string, []attachment.Attachment, error) {
+ confluenceExtension := NewConfluenceLegacyExtension(stdlib, path, cfg)
+ html, err := compileMarkdownWithExtension(markdown, confluenceExtension, "rendering markdown with legacy renderer:\n%s")
+ return html, confluenceExtension.Attachments, err
+}
+
+// ConfluenceExtension is a goldmark extension for GitHub Alerts with Transformer approach
+// This extension provides superior GitHub Alert processing by transforming [!NOTE], [!TIP], etc.
+// into proper Confluence macros while maintaining full compatibility with existing functionality.
+// This is now the primary/default extension.
+type ConfluenceExtension struct {
+ html.Config
+ Stdlib *stdlib.Lib
+ Path string
+ MarkConfig types.MarkConfig
+ Attachments []attachment.Attachment
+}
+
+// NewConfluenceExtension creates a new instance of the GitHub Alerts extension
+// This is the improved standalone version that doesn't depend on feature flags
+func NewConfluenceExtension(stdlib *stdlib.Lib, path string, cfg types.MarkConfig) *ConfluenceExtension {
+ return &ConfluenceExtension{
+ Config: html.NewConfig(),
+ Stdlib: stdlib,
+ Path: path,
+ MarkConfig: cfg,
+ Attachments: []attachment.Attachment{},
+ }
+}
+
+func (c *ConfluenceExtension) Attach(a attachment.Attachment) {
+ c.Attachments = append(c.Attachments, a)
+}
+
+// Extend extends the Goldmark processor with GitHub Alerts transformer and renderers
+// This method registers all necessary components for GitHub Alert processing:
+// 1. Core renderers for standard markdown elements
+// 2. GitHub Alerts specific renderers (blockquote and text) with higher priority
+// 3. GitHub Alerts AST transformer for preprocessing
+func (c *ConfluenceExtension) Extend(m goldmark.Markdown) {
+ // Register core renderers (excluding blockquote and text which we'll replace)
+ m.Renderer().AddOptions(renderer.WithNodeRenderers(
+ util.Prioritized(crenderer.NewConfluenceCodeBlockRenderer(c.Stdlib, c.Path), 100),
+ util.Prioritized(crenderer.NewConfluenceFencedCodeBlockRenderer(c.Stdlib, c, c.MarkConfig), 100),
+ util.Prioritized(crenderer.NewConfluenceHTMLBlockRenderer(c.Stdlib), 100),
+ util.Prioritized(crenderer.NewConfluenceHeadingRenderer(c.MarkConfig.DropFirstH1), 100),
+ util.Prioritized(crenderer.NewConfluenceImageRenderer(c.Stdlib, c, c.Path, c.MarkConfig.ImageAlign), 100),
+ util.Prioritized(crenderer.NewConfluenceParagraphRenderer(), 100),
+ util.Prioritized(crenderer.NewConfluenceLinkRenderer(), 100),
+ util.Prioritized(crenderer.NewConfluenceTaskListRenderer(), 100),
+ ))
+
+ // Add GitHub Alerts specific renderers with higher priority to override defaults
+ // These renderers handle both GitHub Alerts and legacy blockquote syntax
+ m.Renderer().AddOptions(renderer.WithNodeRenderers(
+ util.Prioritized(crenderer.NewConfluenceGHAlertsBlockQuoteRenderer(), 200),
+ util.Prioritized(crenderer.NewConfluenceTextRenderer(c.MarkConfig.StripNewlines), 200),
+ ))
+
+ // Add the GitHub Alerts AST transformer that preprocesses [!TYPE] syntax
+ m.Parser().AddOptions(parser.WithASTTransformers(
+ util.Prioritized(ctransformer.NewGHAlertsTransformer(), 100),
+ ))
+
+ // Add mkdocsadmonitions support if requested
+ if slices.Contains(c.MarkConfig.Features, "mkdocsadmonitions") {
+ m.Parser().AddOptions(
+ parser.WithBlockParsers(
+ util.Prioritized(mkDocsParser.NewAdmonitionParser(), 100),
+ ),
+ )
+
+ m.Renderer().AddOptions(renderer.WithNodeRenderers(
+ util.Prioritized(crenderer.NewConfluenceMkDocsAdmonitionRenderer(), 100),
+ ))
+ }
+
+ // Add mention support if requested
+ if slices.Contains(c.MarkConfig.Features, "mention") {
+ m.Parser().AddOptions(
+ parser.WithInlineParsers(
+ util.Prioritized(cparser.NewMentionParser(), 99),
+ ),
+ )
+
+ m.Renderer().AddOptions(renderer.WithNodeRenderers(
+ util.Prioritized(crenderer.NewConfluenceMentionRenderer(c.Stdlib), 100),
+ ))
+ }
+
+ // Add confluence tag parser for
", "Legacy renderer should treat GitHub Alerts as regular blockquotes")
+ } else {
+ // This is a legacy syntax case (like "info:") - both should produce macro
+ assert.Contains(t, legacyResult, "structured-macro", "Legacy renderer should produce Confluence macro for legacy syntax")
+ }
+ } else {
+ assert.Contains(t, transformerResult, "
", "Regular blockquote should use HTML blockquote")
+ assert.Contains(t, legacyResult, "
", "Regular blockquote should use HTML blockquote")
+ } // Check for GitHub Alert syntax cleanup (only for transformer with GitHub Alert syntax)
+ if tc.expectClean {
+ // Transformer should clean up the [!TYPE] syntax
+ assert.NotContains(t, transformerResult, "[!", "Transformer should remove GitHub Alert syntax markers")
+
+ // Legacy renderer might not clean it up (depending on implementation)
+ // We'll just log what it produces for comparison
+ t.Logf("Transformer result: %s", transformerResult)
+ t.Logf("Legacy result: %s", legacyResult)
+ } else {
+ // For non-GitHub Alert cases, both should behave similarly
+ t.Logf("Transformer result: %s", transformerResult)
+ t.Logf("Legacy result: %s", legacyResult)
+ }
+ })
+ }
+}
+
+func TestBasicTransformerFunctionality(t *testing.T) {
+ testMarkdown := "> [!NOTE]\n> This is a test note."
+
+ stdlib, err := stdlib.New(nil)
+ if err != nil {
+ t.Fatalf("Failed to create stdlib: %v", err)
+ }
+
+ cfg := types.MarkConfig{
+ Features: []string{},
+ StripNewlines: false,
+ DropFirstH1: false,
+ }
+
+ result, attachments, err := mark.CompileMarkdown([]byte(testMarkdown), stdlib, "/test", cfg)
+ assert.NoError(t, err)
+
+ // Basic checks
+ assert.NotEmpty(t, result)
+ assert.Empty(t, attachments)
+ assert.Contains(t, result, "structured-macro")
+
+ // This test should now pass because we fixed the transformer
+ assert.NotContains(t, result, "[!NOTE]", "The GitHub Alert syntax should be cleaned up")
+
+ t.Logf("Transformer result: %s", result)
+}
+
+// TestCompatibilityWithExistingFeatures tests that the transformer approach is fully compatible
+// with existing non-blockquote functionality from the original markdown tests
+func TestCompatibilityWithExistingFeatures(t *testing.T) {
+ testCases := []struct {
+ name string
+ markdown string
+ config types.MarkConfig
+ description string
+ }{
+ {
+ name: "Headers Basic",
+ markdown: `# Header 1
+## Header 2
+### Header 3`,
+ config: types.MarkConfig{
+ Features: []string{},
+ StripNewlines: false,
+ DropFirstH1: false,
+ },
+ description: "Basic header rendering should be identical",
+ },
+ {
+ name: "Headers with DropFirstH1",
+ markdown: `# Header 1
+## Header 2
+### Header 3`,
+ config: types.MarkConfig{
+ Features: []string{},
+ StripNewlines: false,
+ DropFirstH1: true,
+ },
+ description: "Header rendering with DropFirstH1 should be identical",
+ },
+ {
+ name: "Code Blocks",
+ markdown: "`inline code`\n\n```bash\necho \"hello\"\n```",
+ config: types.MarkConfig{
+ Features: []string{},
+ StripNewlines: false,
+ DropFirstH1: false,
+ },
+ description: "Code block rendering should be identical",
+ },
+ {
+ name: "Links and Images",
+ markdown: `[Link](https://example.com)
+
+[Page Link](ac:Page)`,
+ config: types.MarkConfig{
+ Features: []string{},
+ StripNewlines: false,
+ DropFirstH1: false,
+ },
+ description: "Links and images should be rendered identically",
+ },
+ {
+ name: "Tables",
+ markdown: `| Header 1 | Header 2 |
+|----------|----------|
+| Row 1 | Row 2 |`,
+ config: types.MarkConfig{
+ Features: []string{},
+ StripNewlines: false,
+ DropFirstH1: false,
+ },
+ description: "Table rendering should be identical",
+ },
+ {
+ name: "Mixed Content",
+ markdown: `# Title
+
+Some **bold** and *italic* text.
+
+- List item 1
+- List item 2
+
+` + "`inline code`" + ` and:
+
+` + "```javascript\nconsole.log(\"test\");\n```" + `
+
+[Link](https://example.com)`,
+ config: types.MarkConfig{
+ Features: []string{},
+ StripNewlines: false,
+ DropFirstH1: false,
+ },
+ description: "Mixed content should be rendered identically",
+ },
+ {
+ name: "Strip Newlines",
+ markdown: `Line 1
+
+Line 2
+
+
+Line 3`,
+ config: types.MarkConfig{
+ Features: []string{},
+ StripNewlines: true,
+ DropFirstH1: false,
+ },
+ description: "StripNewlines functionality should work identically",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Logf("Testing: %s", tc.description)
+
+ stdlib, err := stdlib.New(nil)
+ if err != nil {
+ t.Fatalf("Failed to create stdlib: %v", err)
+ }
+
+ // Test with GitHub Alerts transformer (primary approach)
+ transformerResult, transformerAttachments, err := mark.CompileMarkdown([]byte(tc.markdown), stdlib, "/test", tc.config)
+ assert.NoError(t, err)
+
+ // Test with legacy renderer (original approach)
+ legacyResult, legacyAttachments, err := mark.CompileMarkdownLegacy([]byte(tc.markdown), stdlib, "/test", tc.config)
+ assert.NoError(t, err)
+
+ // Basic checks
+ assert.NotEmpty(t, transformerResult, "Transformer result should not be empty")
+ assert.NotEmpty(t, legacyResult, "Legacy result should not be empty")
+ assert.Equal(t, len(transformerAttachments), len(legacyAttachments), "Attachment counts should match")
+
+ // The key compatibility test: results should be identical for non-blockquote content
+ if transformerResult != legacyResult {
+ t.Errorf("COMPATIBILITY ISSUE: Results differ for %s\n"+
+ "Transformer result:\n%s\n\n"+
+ "Legacy result:\n%s\n\n"+
+ "Diff (transformer vs legacy):",
+ tc.name, transformerResult, legacyResult)
+
+ // Log the differences for debugging
+ t.Logf("Transformer length: %d", len(transformerResult))
+ t.Logf("Legacy length: %d", len(legacyResult))
+
+ // Character-by-character comparison for debugging
+ for i := 0; i < len(transformerResult) && i < len(legacyResult); i++ {
+ if transformerResult[i] != legacyResult[i] {
+ t.Logf("First difference at position %d: transformer='%c'(%d) vs legacy='%c'(%d)",
+ i, transformerResult[i], transformerResult[i], legacyResult[i], legacyResult[i])
+ break
+ }
+ }
+ } else {
+ t.Logf("✅ Perfect compatibility for %s", tc.name)
+ }
+ })
+ }
+}
diff --git a/renderer/blockquote.go b/renderer/blockquote.go
index 9489cd5..1c4a3ef 100644
--- a/renderer/blockquote.go
+++ b/renderer/blockquote.go
@@ -64,18 +64,9 @@ func LegacyBlockQuoteClassifier() BlockQuoteClassifier {
}
}
-func GHAlertsBlockQuoteClassifier() BlockQuoteClassifier {
- return BlockQuoteClassifier{
- patternMap: map[string]*regexp.Regexp{
- "info": regexp.MustCompile(`(?i)^\!(note|important)`),
- "note": regexp.MustCompile(`(?i)^\!warning`),
- "warn": regexp.MustCompile(`(?i)^\!caution`),
- "tip": regexp.MustCompile(`(?i)^\!tip`),
- },
- }
-}
-
// ClassifyingBlockQuote compares a string against a set of patterns and returns a BlockQuoteType
+// Note: GitHub Alerts ([!NOTE], [!TIP], etc.) are now handled by the superior transformer approach
+// in the GitHub Alerts extension, not by this legacy blockquote renderer
func (classifier BlockQuoteClassifier) ClassifyingBlockQuote(literal string) BlockQuoteType {
var t = None
@@ -93,10 +84,11 @@ func (classifier BlockQuoteClassifier) ClassifyingBlockQuote(literal string) Blo
}
// ParseBlockQuoteType parses the first line of a blockquote and returns its type
+// Note: This legacy function only handles traditional "info:", "note:", etc. syntax
+// GitHub Alerts ([!NOTE], [!TIP], etc.) are handled by the GitHub Alerts transformer
func ParseBlockQuoteType(node ast.Node, source []byte) BlockQuoteType {
var t = None
var legacyClassifier = LegacyBlockQuoteClassifier()
- var ghAlertsClassifier = GHAlertsBlockQuoteClassifier()
countParagraphs := 0
_ = ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
@@ -109,27 +101,6 @@ func ParseBlockQuoteType(node ast.Node, source []byte) BlockQuoteType {
if node.Kind() == ast.KindText {
n := node.(*ast.Text)
t = legacyClassifier.ClassifyingBlockQuote(string(n.Value(source)))
- // If the node is a text node but classification returned none do not give up!
- // Find the next two sibling nodes midNode and rightNode,
- // 1. If both are also a text node
- // 2. and the original node (node) text value is '['
- // 3. and the rightNode text value is ']'
- // It means with high degree of confidence that the original md doc contains a Github alert type of blockquote
- // Classifying the next text type node (midNode) will confirm that.
- if t == None {
- midNode := node.NextSibling()
-
- if midNode != nil && midNode.Kind() == ast.KindText {
- rightNode := midNode.NextSibling()
- midTextNode := midNode.(*ast.Text)
- if rightNode != nil && rightNode.Kind() == ast.KindText {
- rightTextNode := rightNode.(*ast.Text)
- if string(n.Value(source)) == "[" && string(rightTextNode.Value(source)) == "]" {
- t = ghAlertsClassifier.ClassifyingBlockQuote(string(midTextNode.Value(source)))
- }
- }
- }
- }
countParagraphs += 1
}
if node.Kind() == ast.KindHTMLBlock {
diff --git a/renderer/gh_alerts_blockquote.go b/renderer/gh_alerts_blockquote.go
new file mode 100644
index 0000000..157dc6d
--- /dev/null
+++ b/renderer/gh_alerts_blockquote.go
@@ -0,0 +1,150 @@
+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("
\n"); err != nil {
+ return ast.WalkStop, err
+ }
+ } else {
+ if _, err := writer.WriteString("
\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("\n"); err != nil {
+ return ast.WalkStop, err
+ }
+ } else {
+ if _, err := writer.WriteString("
\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("\n"); err != nil {
+ return ast.WalkStop, err
+ }
+ } else {
+ if _, err := writer.WriteString("
\n"); err != nil {
+ return ast.WalkStop, err
+ }
+ }
+ }
+
+ return ast.WalkContinue, nil
+}
diff --git a/renderer/text.go b/renderer/text.go
index a8c6576..c1670e3 100644
--- a/renderer/text.go
+++ b/renderer/text.go
@@ -10,23 +10,15 @@ import (
"github.com/yuin/goldmark/util"
)
-// ConfluenceTextRenderer slightly alters the default goldmark behavior for
-// inline text block. It allows for soft breaks
-// (c.f. https://spec.commonmark.org/0.30/#softbreak)
-// to be rendered into HTML as either '\n' (the goldmark default)
-// or as ' '.
-// This latter option is useful for Confluence,
-// which inserts
tags into uploaded HTML where it sees '\n'.
-// See also https://sembr.org/ for partial motivation.
type ConfluenceTextRenderer struct {
html.Config
softBreak rune
}
-// NewConfluenceTextRenderer creates a new instance of the ConfluenceTextRenderer
-func NewConfluenceTextRenderer(stripNL bool, opts ...html.Option) renderer.NodeRenderer {
+// NewConfluenceTextRenderer creates a new instance of the renderer with GitHub Alerts support
+func NewConfluenceTextRenderer(stripNewlines bool, opts ...html.Option) renderer.NodeRenderer {
sb := '\n'
- if stripNL {
+ if stripNewlines {
sb = ' '
}
return &ConfluenceTextRenderer{
@@ -35,18 +27,36 @@ func NewConfluenceTextRenderer(stripNL bool, opts ...html.Option) renderer.NodeR
}
}
-// RegisterFuncs implements NodeRenderer.RegisterFuncs .
+// RegisterFuncs implements NodeRenderer.RegisterFuncs
func (r *ConfluenceTextRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindText, r.renderText)
}
-// This is taken from https://github.com/yuin/goldmark/blob/v1.6.0/renderer/html/html.go#L719
-// with the hardcoded '\n' for soft breaks swapped for the configurable r.softBreak
+// renderText handles text rendering and supports GitHub Alerts replacement content.
+// This is an enhanced version of the default goldmark text renderer that checks
+// for replacement-content attributes before falling back to standard behavior.
+// Note: This logic is partially duplicated from ConfluenceTextLegacyRenderer.renderText
+// but includes additional GitHub Alerts support. We keep them separate to maintain
+// clean legacy vs enhanced implementation paths.
func (r *ConfluenceTextRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
+
n := node.(*ast.Text)
+
+ // Check if this text node has replacement content from the GHAlerts transformer
+ if replacementContent, hasAttribute := node.Attribute([]byte("replacement-content")); hasAttribute && replacementContent != nil {
+ if contentBytes, ok := replacementContent.([]byte); ok {
+ _, err := w.Write(contentBytes)
+ if err != nil {
+ return ast.WalkStop, err
+ }
+ return ast.WalkContinue, nil
+ }
+ }
+
+ // Default text rendering behavior (same as original ConfluenceTextRenderer)
segment := n.Segment
if n.IsRaw() {
r.Writer.RawWrite(w, segment.Value(source))
@@ -87,6 +97,7 @@ func (r *ConfluenceTextRenderer) renderText(w util.BufWriter, source []byte, nod
}
}
}
+
return ast.WalkContinue, nil
}
diff --git a/renderer/text_legacy.go b/renderer/text_legacy.go
new file mode 100644
index 0000000..2be8fb7
--- /dev/null
+++ b/renderer/text_legacy.go
@@ -0,0 +1,90 @@
+package renderer
+
+import (
+ "unicode/utf8"
+
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/util"
+)
+
+// ConfluenceTextLegacyRenderer slightly alters the default goldmark behavior for
+// inline text block. It allows for soft breaks
+// (c.f. https://spec.commonmark.org/0.30/#softbreak)
+// to be rendered into HTML as either '\n' (the goldmark default)
+// or as ' '.
+// This latter option is useful for Confluence,
+// which inserts
tags into uploaded HTML where it sees '\n'.
+// See also https://sembr.org/ for partial motivation.
+type ConfluenceTextLegacyRenderer struct {
+ html.Config
+ softBreak rune
+}
+
+// NewConfluenceTextLegacyRenderer creates a new instance of the ConfluenceTextRenderer (legacy version)
+func NewConfluenceTextLegacyRenderer(stripNL bool, opts ...html.Option) renderer.NodeRenderer {
+ sb := '\n'
+ if stripNL {
+ sb = ' '
+ }
+ return &ConfluenceTextLegacyRenderer{
+ Config: html.NewConfig(),
+ softBreak: sb,
+ }
+}
+
+// RegisterFuncs implements NodeRenderer.RegisterFuncs .
+func (r *ConfluenceTextLegacyRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ reg.Register(ast.KindText, r.renderText)
+}
+
+// This is taken from https://github.com/yuin/goldmark/blob/v1.6.0/renderer/html/html.go#L719
+// with the hardcoded '\n' for soft breaks swapped for the configurable r.softBreak
+func (r *ConfluenceTextLegacyRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ n := node.(*ast.Text)
+ segment := n.Segment
+ if n.IsRaw() {
+ r.Writer.RawWrite(w, segment.Value(source))
+ } else {
+ value := segment.Value(source)
+ r.Writer.Write(w, value)
+ if n.HardLineBreak() || (n.SoftLineBreak() && r.HardWraps) {
+ if r.XHTML {
+ _, _ = w.WriteString("
\n")
+ } else {
+ _, _ = w.WriteString("
\n")
+ }
+ } else if n.SoftLineBreak() {
+ if r.EastAsianLineBreaks != html.EastAsianLineBreaksNone && len(value) != 0 {
+ sibling := node.NextSibling()
+ if sibling != nil && sibling.Kind() == ast.KindText {
+ if siblingText := sibling.(*ast.Text).Value(source); len(siblingText) != 0 {
+ thisLastRune := util.ToRune(value, len(value)-1)
+ siblingFirstRune, _ := utf8.DecodeRune(siblingText)
+ // Inline the softLineBreak function as it's not public
+ writeLineBreak := false
+ switch r.EastAsianLineBreaks {
+ case html.EastAsianLineBreaksNone:
+ writeLineBreak = false
+ case html.EastAsianLineBreaksSimple:
+ writeLineBreak = !util.IsEastAsianWideRune(thisLastRune) || !util.IsEastAsianWideRune(siblingFirstRune)
+ case html.EastAsianLineBreaksCSS3Draft:
+ writeLineBreak = eastAsianLineBreaksCSS3DraftSoftLineBreak(thisLastRune, siblingFirstRune)
+ }
+
+ if writeLineBreak {
+ _ = w.WriteByte(byte(r.softBreak))
+ }
+ }
+ }
+ } else {
+ _ = w.WriteByte(byte(r.softBreak))
+ }
+ }
+ }
+ return ast.WalkContinue, nil
+}
diff --git a/testdata/quotes-droph1.html b/testdata/quotes-droph1.html
index 3d7d2de..a6e3def 100644
--- a/testdata/quotes-droph1.html
+++ b/testdata/quotes-droph1.html
@@ -48,7 +48,7 @@ b
[!NOTE]
+Note
[!TIP]
+Tip
[!WARNING]
+Warning
[!IMPORTANT]
+Important
[!CAUTION]
+Caution
[!NOTE]
+Note
[!TIP]
+Tip
[!WARNING]
+Warning
[!IMPORTANT]
+Important
[!CAUTION]
+Caution
[!NOTE]
+Note
[!TIP]
+Tip
[!WARNING]
+Warning
[!IMPORTANT]
+Important
[!CAUTION]
+Caution
Note
+This is a note alert with markdown formatting.
+