Repository: kovetskiy/mark (go)

Issue:
Support Github Alerts Markdown
**Is your feature request related to a problem? Please describe.**
`mark` supports MD blockquote admonitions conversion to Confluence Info, Tip, Note, and Warning Macros but fails to do so for [Github MD Alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts). 

**Describe the solution you'd like**
Add support for `Github Alerts` type of MD Admonitions.


Pull request:
To add initial support for github md alerts
Closes #494 

Patch:
diff --git a/README.md b/README.md
index 1c64f44..faed052 100644
--- a/README.md
+++ b/README.md
@@ -262,7 +262,15 @@ some long code block
 Block Quotes are converted to Confluence Info/Warn/Note box when the following conditions are met
 
 1. The BlockQuote is on the root level of the document (not nested)
-1. The first line of the BlockQuote contains one of the following patterns `Info/Warn/Note`
+1. The first line of the BlockQuote contains one of the following patterns `Info/Warn/Note` or [Github MD Alerts style](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) `[!NOTE]/[!TIP]/[!IMPORTANT]/[!WARNING]/[!CAUTION]`
+
+| Github Alerts | Confluence |
+|---------------|------------|
+| Tip (green lightbulb)   | Tip (green checkmark in circle) |
+| Note (blue I in circle) | Info (blue I in circle) |
+| Important (purple exclamation mark in speech bubble) | Info (blue I in circle) |
+| Warning (yellow exclamation mark in triangle) | Note (yellow exclamation mark in triangle) |
+| Caution (red exclamation mark in hexagon) | Warning (red exclamation mark in hexagon) |
 
 In any other case the default behaviour will be resumed and html `<blockquote>` tag will be used
 
diff --git a/pkg/mark/renderer/blockquote.go b/pkg/mark/renderer/blockquote.go
index 9d21573..12100b3 100644
--- a/pkg/mark/renderer/blockquote.go
+++ b/pkg/mark/renderer/blockquote.go
@@ -35,11 +35,12 @@ const (
 	Info BlockQuoteType = iota
 	Note
 	Warn
+	Tip
 	None
 )
 
 func (t BlockQuoteType) String() string {
-	return []string{"info", "note", "warn", "none"}[t]
+	return []string{"info", "note", "warning", "tip", "none"}[t]
 }
 
 type BlockQuoteLevelMap map[ast.Node]int
@@ -48,20 +49,45 @@ func (m BlockQuoteLevelMap) Level(node ast.Node) int {
 	return m[node]
 }
 
+type BlockQuoteClassifier struct {
+	patternMap map[string]*regexp.Regexp
+}
+
+func LegacyBlockQuoteClassifier() BlockQuoteClassifier {
+	return BlockQuoteClassifier{
+		patternMap: map[string]*regexp.Regexp{
+			"info": regexp.MustCompile(`(?i)info`),
+			"note": regexp.MustCompile(`(?i)note`),
+			"warn": regexp.MustCompile(`(?i)warn`),
+			"tip":  regexp.MustCompile(`(?i)tip`),
+		},
+	}
+}
+
+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
-func ClassifyingBlockQuote(literal string) BlockQuoteType {
-	infoPattern := regexp.MustCompile(`info|Info|INFO`)
-	notePattern := regexp.MustCompile(`note|Note|NOTE`)
-	warnPattern := regexp.MustCompile(`warn|Warn|WARN`)
+func (classifier BlockQuoteClassifier) ClassifyingBlockQuote(literal string) BlockQuoteType {
 
 	var t = None
 	switch {
-	case infoPattern.MatchString(literal):
+	case classifier.patternMap["info"].MatchString(literal):
 		t = Info
-	case notePattern.MatchString(literal):
+	case classifier.patternMap["note"].MatchString(literal):
 		t = Note
-	case warnPattern.MatchString(literal):
+	case classifier.patternMap["warn"].MatchString(literal):
 		t = Warn
+	case classifier.patternMap["tip"].MatchString(literal):
+		t = Tip
 	}
 	return t
 }
@@ -69,6 +95,8 @@ func ClassifyingBlockQuote(literal string) BlockQuoteType {
 // ParseBlockQuoteType parses the first line of a blockquote and returns its type
 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) {
@@ -80,7 +108,29 @@ func ParseBlockQuoteType(node ast.Node, source []byte) BlockQuoteType {
 		if countParagraphs < 2 && entering {
 			if node.Kind() == ast.KindText {
 				n := node.(*ast.Text)
-				t = ClassifyingBlockQuote(string(n.Text(source)))
+				t = legacyClassifier.ClassifyingBlockQuote(string(n.Text(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()
+					rightNode := midNode.NextSibling()
+
+					if midNode.Kind() == ast.KindText {
+						midTextNode := midNode.(*ast.Text)
+						if rightNode != nil && rightNode.Kind() == ast.KindText {
+							rightTextNode := rightNode.(*ast.Text)
+
+							if string(n.Text(source)) == "[" && string(rightTextNode.Text(source)) == "]" {
+								t = ghAlertsClassifier.ClassifyingBlockQuote(string(midTextNode.Text(source)))
+							}
+						}
+					}
+				}
 				countParagraphs += 1
 			}
 			if node.Kind() == ast.KindHTMLBlock {
@@ -88,7 +138,7 @@ func ParseBlockQuoteType(node ast.Node, source []byte) BlockQuoteType {
 				n := node.(*ast.HTMLBlock)
 				for i := 0; i < n.BaseBlock.Lines().Len(); i++ {
 					line := n.BaseBlock.Lines().At(i)
-					t = ClassifyingBlockQuote(string(line.Value(source)))
+					t = legacyClassifier.ClassifyingBlockQuote(string(line.Value(source)))
 					if t != None {
 						break
 					}


Test patch:
diff --git a/pkg/mark/testdata/quotes-droph1.html b/pkg/mark/testdata/quotes-droph1.html
index d8fdcdf..8b98bc4 100644
--- a/pkg/mark/testdata/quotes-droph1.html
+++ b/pkg/mark/testdata/quotes-droph1.html
@@ -12,7 +12,7 @@ b</p>
 <p><strong>Warn (Should not be picked as blockquote type)</strong></p>
 </ac:rich-text-body></ac:structured-macro>
 <h2 id="second-heading">Second Heading</h2>
-<ac:structured-macro ac:name="warn"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
 <p><strong>Warn</strong></p>
 <ul>
 <li>Warn bullet 1</li>
@@ -28,7 +28,73 @@ that runs long</li>
 <!-- Info -->
 <p>Test</p>
 </ac:rich-text-body></ac:structured-macro>
+<h2 id="fourth-heading---warn-should-not-get-picked-as-block-quote">Fourth Heading - Warn should not get picked as block quote</h2>
+<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p><strong>TIP:</strong></p>
+<ol>
+<li>Note number one</li>
+<li>Note number two</li>
+</ol>
+<blockquote>
+<p>a
+b</p>
+</blockquote>
+<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
+</ac:rich-text-body></ac:structured-macro>
 <h2 id="simple-blockquote">Simple Blockquote</h2>
 <blockquote>
 <p>This paragraph is a simple blockquote</p>
 </blockquote>
+<h2 id="gh-alerts-heading">GH Alerts Heading</h2>
+<h3 id="note-type-alert-heading">Note Type Alert Heading</h3>
+<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!NOTE]</p>
+<ul>
+<li>Note bullet 1</li>
+<li>Note bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<h3 id="tip-type-alert-heading">Tip Type Alert Heading</h3>
+<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!TIP]</p>
+<ul>
+<li>Tip bullet 1</li>
+<li>Tip bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<h3 id="warning-type-alert-heading">Warning Type Alert Heading</h3>
+<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!WARNING]</p>
+<ul>
+<li>Warning bullet 1</li>
+<li>Warning bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<h3 id="importantcaution-type-alert-heading">Important/Caution Type Alert Heading</h3>
+<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!IMPORTANT]</p>
+<ul>
+<li>Important bullet 1</li>
+<li>Important bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!CAUTION]</p>
+<ul>
+<li>Important bullet 1</li>
+<li>Important bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<h3 id="should-not-be-picked-up-and-converted-into-blockquote-macro">Should not be picked up and converted into blockquote macro</h3>
+<blockquote>
+<p>[[!NOTE]</p>
+</blockquote>
+<blockquote>
+<p>[!NOTE</p>
+</blockquote>
+<blockquote>
+<p>[Hey !NOTE]</p>
+</blockquote>
+<blockquote>
+<p>[NOTE]</p>
+</blockquote>
diff --git a/pkg/mark/testdata/quotes-stripnewlines.html b/pkg/mark/testdata/quotes-stripnewlines.html
index 7a43019..ba9c973 100644
--- a/pkg/mark/testdata/quotes-stripnewlines.html
+++ b/pkg/mark/testdata/quotes-stripnewlines.html
@@ -12,7 +12,7 @@
 <p><strong>Warn (Should not be picked as blockquote type)</strong></p>
 </ac:rich-text-body></ac:structured-macro>
 <h2 id="second-heading">Second Heading</h2>
-<ac:structured-macro ac:name="warn"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
 <p><strong>Warn</strong></p>
 <ul>
 <li>Warn bullet 1</li>
@@ -27,7 +27,72 @@
 <!-- Info -->
 <p>Test</p>
 </ac:rich-text-body></ac:structured-macro>
+<h2 id="fourth-heading---warn-should-not-get-picked-as-block-quote">Fourth Heading - Warn should not get picked as block quote</h2>
+<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p><strong>TIP:</strong></p>
+<ol>
+<li>Note number one</li>
+<li>Note number two</li>
+</ol>
+<blockquote>
+<p>a b</p>
+</blockquote>
+<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
+</ac:rich-text-body></ac:structured-macro>
 <h2 id="simple-blockquote">Simple Blockquote</h2>
 <blockquote>
 <p>This paragraph is a simple blockquote</p>
 </blockquote>
+<h2 id="gh-alerts-heading">GH Alerts Heading</h2>
+<h3 id="note-type-alert-heading">Note Type Alert Heading</h3>
+<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!NOTE]</p>
+<ul>
+<li>Note bullet 1</li>
+<li>Note bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<h3 id="tip-type-alert-heading">Tip Type Alert Heading</h3>
+<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!TIP]</p>
+<ul>
+<li>Tip bullet 1</li>
+<li>Tip bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<h3 id="warning-type-alert-heading">Warning Type Alert Heading</h3>
+<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!WARNING]</p>
+<ul>
+<li>Warning bullet 1</li>
+<li>Warning bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<h3 id="importantcaution-type-alert-heading">Important/Caution Type Alert Heading</h3>
+<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!IMPORTANT]</p>
+<ul>
+<li>Important bullet 1</li>
+<li>Important bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!CAUTION]</p>
+<ul>
+<li>Important bullet 1</li>
+<li>Important bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<h3 id="should-not-be-picked-up-and-converted-into-blockquote-macro">Should not be picked up and converted into blockquote macro</h3>
+<blockquote>
+<p>[[!NOTE]</p>
+</blockquote>
+<blockquote>
+<p>[!NOTE</p>
+</blockquote>
+<blockquote>
+<p>[Hey !NOTE]</p>
+</blockquote>
+<blockquote>
+<p>[NOTE]</p>
+</blockquote>
diff --git a/pkg/mark/testdata/quotes.html b/pkg/mark/testdata/quotes.html
index c1519c7..a2d2a00 100644
--- a/pkg/mark/testdata/quotes.html
+++ b/pkg/mark/testdata/quotes.html
@@ -13,7 +13,7 @@ b</p>
 <p><strong>Warn (Should not be picked as blockquote type)</strong></p>
 </ac:rich-text-body></ac:structured-macro>
 <h2 id="second-heading">Second Heading</h2>
-<ac:structured-macro ac:name="warn"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
 <p><strong>Warn</strong></p>
 <ul>
 <li>Warn bullet 1</li>
@@ -29,7 +29,73 @@ that runs long</li>
 <!-- Info -->
 <p>Test</p>
 </ac:rich-text-body></ac:structured-macro>
+<h2 id="fourth-heading---warn-should-not-get-picked-as-block-quote">Fourth Heading - Warn should not get picked as block quote</h2>
+<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p><strong>TIP:</strong></p>
+<ol>
+<li>Note number one</li>
+<li>Note number two</li>
+</ol>
+<blockquote>
+<p>a
+b</p>
+</blockquote>
+<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
+</ac:rich-text-body></ac:structured-macro>
 <h2 id="simple-blockquote">Simple Blockquote</h2>
 <blockquote>
 <p>This paragraph is a simple blockquote</p>
 </blockquote>
+<h2 id="gh-alerts-heading">GH Alerts Heading</h2>
+<h3 id="note-type-alert-heading">Note Type Alert Heading</h3>
+<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!NOTE]</p>
+<ul>
+<li>Note bullet 1</li>
+<li>Note bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<h3 id="tip-type-alert-heading">Tip Type Alert Heading</h3>
+<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!TIP]</p>
+<ul>
+<li>Tip bullet 1</li>
+<li>Tip bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<h3 id="warning-type-alert-heading">Warning Type Alert Heading</h3>
+<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!WARNING]</p>
+<ul>
+<li>Warning bullet 1</li>
+<li>Warning bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<h3 id="importantcaution-type-alert-heading">Important/Caution Type Alert Heading</h3>
+<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!IMPORTANT]</p>
+<ul>
+<li>Important bullet 1</li>
+<li>Important bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p>[!CAUTION]</p>
+<ul>
+<li>Important bullet 1</li>
+<li>Important bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
+<h3 id="should-not-be-picked-up-and-converted-into-blockquote-macro">Should not be picked up and converted into blockquote macro</h3>
+<blockquote>
+<p>[[!NOTE]</p>
+</blockquote>
+<blockquote>
+<p>[!NOTE</p>
+</blockquote>
+<blockquote>
+<p>[Hey !NOTE]</p>
+</blockquote>
+<blockquote>
+<p>[NOTE]</p>
+</blockquote>
diff --git a/pkg/mark/testdata/quotes.md b/pkg/mark/testdata/quotes.md
index 995f67f..d9de8e0 100644
--- a/pkg/mark/testdata/quotes.md
+++ b/pkg/mark/testdata/quotes.md
@@ -26,6 +26,65 @@
 > <!-- Info -->
 > Test
 
+## Fourth Heading - Warn should not get picked as block quote
+
+> **TIP:**
+>
+> 1. Note number one
+> 1. Note number two
+>
+>> a
+>> b
+>
+> **Warn (Should not be picked as blockquote type)**
+
 ## Simple Blockquote
 
 > This paragraph is a simple blockquote
+
+## GH Alerts Heading
+
+### Note Type Alert Heading
+
+> [!NOTE]
+>
+> * Note bullet 1
+> * Note bullet 2
+
+### Tip Type Alert Heading
+
+> [!TIP]
+>
+> * Tip bullet 1
+> * Tip bullet 2
+
+### Warning Type Alert Heading
+
+> [!WARNING]
+>
+> * Warning bullet 1
+> * Warning bullet 2
+
+### Important/Caution Type Alert Heading
+
+> [!IMPORTANT]
+>
+> * Important bullet 1
+> * Important bullet 2
+
+
+> [!CAUTION]
+>
+> * Important bullet 1
+> * Important bullet 2
+
+### Should not be picked up and converted into blockquote macro
+
+> [[!NOTE]
+
+
+> [!NOTE
+
+> [Hey !NOTE]
+
+> [NOTE]