Repository: kovetskiy/mark (go)

Issue:
Add support for coloured panels
Github markdown [added a feature](https://github.com/github/feedback/discussions/16925) to support highlighting (`note` and `warning`) using blockquotes

> **Note**
> This is a note

> **Warning**
> This is a warning

Can we add the support for this to be automatically converted to the coloured panels on Confluence.
Right now we can do this using macros, like
```markdown
<!-- Macro: :box:([^:]+):([^:]*):(.+):
     Template: ac:box
     Icon: true
     Name: ${1}
     Title: ${2}
     Body: ${3} -->

:box:info::Foobar:
:box:tip:Tip of day:Foobar:
:box:note::Foobar:
:box:warning:Alert!:Foobar:
```
But would be nice to have blockquotes, converted to coloured panels on confluence without using the macros.

Related issue: https://github.com/kovetskiy/mark/issues/48

Pull request:
Feature/prokod/#194
Hi,

- I have been using `mark` for some time and I liked the idea a lot. I was missing badly #194 feature - In our enterprise confluence blockquote is just shown as plain quote marks ... which really was annoying and also using the templates/macros was breaking markdown when viewing it using github.
- So I have put my hands in trying to contribute on that front
- This is my first go at go :) so sorry if this is not idiomatic enough
- I have added simple test case also by adding quotes.* under testdata
- It is a base implementation and could be refined of course
- I have tested it in enterprise confluence and it looks like it is working :)
- Should imo close #194

Patch:
diff --git a/pkg/mark/markdown.go b/pkg/mark/markdown.go
index baa9225..679a1fb 100644
--- a/pkg/mark/markdown.go
+++ b/pkg/mark/markdown.go
@@ -1,6 +1,7 @@
 package mark
 
 import (
+	"fmt"
 	"io"
 	"regexp"
 	"strings"
@@ -8,7 +9,6 @@ import (
 	bf "github.com/kovetskiy/blackfriday/v2"
 	"github.com/kovetskiy/mark/pkg/mark/stdlib"
 	"github.com/reconquest/pkg/log"
-	"fmt"
 )
 
 var reBlockDetails = regexp.MustCompile(
@@ -17,10 +17,123 @@ var reBlockDetails = regexp.MustCompile(
 	`^(?:(\w*)|-)\s*\b(\S.*?\S?)??\s*(?:\btitle\s+(\S.*\S?))?$`,
 )
 
+type BlockQuoteLevelMap map[*bf.Node]int
+
+func (m BlockQuoteLevelMap) Level(node *bf.Node) int {
+	return m[node]
+}
+
 type ConfluenceRenderer struct {
 	bf.Renderer
 
 	Stdlib *stdlib.Lib
+
+	LevelMap BlockQuoteLevelMap
+}
+
+func ParseLanguage(lang string) string {
+	// lang takes the following form: language? "collapse"? ("title"? <any string>*)?
+	// let's split it by spaces
+	paramlist := strings.Fields(lang)
+
+	// get the word in question, aka the first one
+	first := lang
+	if len(paramlist) > 0 {
+		first = paramlist[0]
+	}
+
+	if first == "collapse" || first == "title" {
+		// collapsing or including a title without a language
+		return ""
+	}
+	// the default case with language being the first one
+	return first
+}
+
+func ParseTitle(lang string) string {
+	index := strings.Index(lang, "title")
+	if index >= 0 {
+		// it's found, check if title is given and return it
+		start := index + 6
+		if len(lang) > start {
+			return lang[start:]
+		}
+	}
+	return ""
+}
+
+// Define BlockQuoteType enum
+type BlockQuoteType int
+
+const (
+	Info BlockQuoteType = iota
+	Note
+	Warn
+	None
+)
+
+func (t BlockQuoteType) String() string {
+	return []string{"info", "note", "warn", "none"}[t]
+}
+
+func ClasifyingBlockQuote(literal string) BlockQuoteType {
+	infoPattern := regexp.MustCompile(`info|Info|INFO`)
+	notePattern := regexp.MustCompile(`note|Note|NOTE`)
+	warnPattern := regexp.MustCompile(`warn|Warn|WARN`)
+
+	var t BlockQuoteType = None
+	switch {
+	case infoPattern.MatchString(literal):
+		t = Info
+	case notePattern.MatchString(literal):
+		t = Note
+	case warnPattern.MatchString(literal):
+		t = Warn
+	}
+	return t
+}
+
+func ParseBlockQuoteType(node *bf.Node) BlockQuoteType {
+	var t BlockQuoteType = None
+
+	countParagraphs := 0
+	node.Walk(func(node *bf.Node, entering bool) bf.WalkStatus {
+
+		if node.Type == bf.Paragraph && entering {
+			countParagraphs += 1
+		}
+		// Type of block quote should be defined on the first blockquote line
+		if node.Type == bf.Text && countParagraphs < 2 {
+			t = ClasifyingBlockQuote(string(node.Literal))
+		} else if countParagraphs > 1 {
+			return bf.Terminate
+		}
+		return bf.GoToNext
+	})
+	return t
+}
+
+func GenerateBlockQuoteLevel(someNode *bf.Node) BlockQuoteLevelMap {
+
+	// We define state variable that track BlockQuote level while we walk the tree
+	blockQuoteLevel := 0
+	blockQuoteLevelMap := make(map[*bf.Node]int)
+
+	rootNode := someNode
+	for rootNode.Parent != nil {
+		rootNode = rootNode.Parent
+	}
+	rootNode.Walk(func(node *bf.Node, entering bool) bf.WalkStatus {
+		if node.Type == bf.BlockQuote && entering {
+			blockQuoteLevelMap[node] = blockQuoteLevel
+			blockQuoteLevel += 1
+		}
+		if node.Type == bf.BlockQuote && !entering {
+			blockQuoteLevel -= 1
+		}
+		return bf.GoToNext
+	})
+	return blockQuoteLevelMap
 }
 
 func (renderer ConfluenceRenderer) RenderNode(
@@ -28,9 +141,14 @@ func (renderer ConfluenceRenderer) RenderNode(
 	node *bf.Node,
 	entering bool,
 ) bf.WalkStatus {
+	// Initialize BlockQuote level map
+	if renderer.LevelMap == nil {
+		renderer.LevelMap = GenerateBlockQuoteLevel(node)
+	}
+
 	if node.Type == bf.CodeBlock {
 
-		groups:= reBlockDetails.FindStringSubmatch(string(node.Info))
+		groups := reBlockDetails.FindStringSubmatch(string(node.Info))
 		linenumbers := false
 		firstline := 0
 		theme := ""
@@ -97,6 +215,32 @@ func (renderer ConfluenceRenderer) RenderNode(
 		}
 		return bf.GoToNext
 	}
+	if node.Type == bf.BlockQuote {
+		quoteType := ParseBlockQuoteType(node)
+		quoteLevel := renderer.LevelMap.Level(node)
+
+		re := regexp.MustCompile(`[\n\t]`)
+
+		if quoteLevel == 0 && entering {
+			if _, err := writer.Write([]byte(re.ReplaceAllString(fmt.Sprintf(`
+			<ac:structured-macro ac:name="%s">
+			<ac:parameter ac:name="icon">true</ac:parameter>
+			<ac:rich-text-body>
+			`, quoteType), ""))); err != nil {
+				panic(err)
+			}
+			return bf.GoToNext
+		}
+		if quoteLevel == 0 && !entering {
+			if _, err := writer.Write([]byte(re.ReplaceAllString(`
+			</ac:rich-text-body>
+			</ac:structured-macro>
+			`, ""))); err != nil {
+				panic(err)
+			}
+			return bf.GoToNext
+		}
+	}
 	return renderer.Renderer.RenderNode(writer, node, entering)
 }
 
@@ -129,8 +273,8 @@ func CompileMarkdown(
 					bf.SmartypantsLatexDashes,
 			},
 		),
-
-		Stdlib: stdlib,
+		Stdlib:   stdlib,
+		LevelMap: nil,
 	}
 
 	html := bf.Run(


Test patch:
diff --git a/pkg/mark/testdata/quotes.html b/pkg/mark/testdata/quotes.html
new file mode 100644
index 0000000..d9819a5
--- /dev/null
+++ b/pkg/mark/testdata/quotes.html
@@ -0,0 +1,27 @@
+<h1 id="main-heading">Main Heading</h1>
+
+<h2 id="first-heading">First Heading</h2>
+<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p><strong>NOTES:</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="second-heading">Second Heading</h2>
+<ac:structured-macro ac:name="warn"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
+<p><strong>Warn</strong></p>
+
+<ul>
+<li>Warn bullet 1</li>
+<li>Warn bullet 2</li>
+</ul>
+</ac:rich-text-body></ac:structured-macro>
\ No newline at end of file
diff --git a/pkg/mark/testdata/quotes.md b/pkg/mark/testdata/quotes.md
new file mode 100644
index 0000000..7f23b73
--- /dev/null
+++ b/pkg/mark/testdata/quotes.md
@@ -0,0 +1,20 @@
+# Main Heading
+
+## First Heading
+
+> **NOTES:**
+>
+> 1. Note number one
+> 1. Note number two
+>
+>> a
+>> b
+>
+> **Warn (Should not be picked as blockquote type)**
+
+## Second Heading
+
+> **Warn**
+>
+> * Warn bullet 1
+> * Warn bullet 2