jira-cli/pkg/adf/markdown.go

293 lines
6.1 KiB
Go

package adf
import (
"fmt"
"strings"
)
type nodeTypeHook map[NodeType]func(Connector) string
// MarkdownTranslator is a markdown translator.
type MarkdownTranslator struct {
table struct {
rows int
cols int
ccol int // current column count
sep bool
}
list struct {
ol, ul map[int]bool
depthO int
depthU int
counter map[int]int // each level starts with same numeric counter at the moment.
}
openHooks nodeTypeHook
closeHooks nodeTypeHook
}
// MarkdownTranslatorOption is a functional option for MarkdownTranslator.
type MarkdownTranslatorOption func(*MarkdownTranslator)
// NewMarkdownTranslator constructs markdown translator.
func NewMarkdownTranslator(opts ...MarkdownTranslatorOption) *MarkdownTranslator {
tr := MarkdownTranslator{
list: struct {
ol, ul map[int]bool
depthO int
depthU int
counter map[int]int
}{
ol: make(map[int]bool),
ul: make(map[int]bool),
counter: make(map[int]int),
},
}
for _, opt := range opts {
opt(&tr)
}
return &tr
}
// WithMarkdownOpenHooks sets open hooks of a markdown translator.
func WithMarkdownOpenHooks(hooks nodeTypeHook) MarkdownTranslatorOption {
return func(tr *MarkdownTranslator) {
tr.openHooks = hooks
}
}
// WithMarkdownCloseHooks sets close hooks of a markdown translator.
func WithMarkdownCloseHooks(hooks nodeTypeHook) MarkdownTranslatorOption {
return func(tr *MarkdownTranslator) {
tr.closeHooks = hooks
}
}
// Open implements TagOpener interface.
//
//nolint:gocyclo
func (tr *MarkdownTranslator) Open(n Connector, _ int) string {
var tag strings.Builder
nt, attrs := n.GetType(), n.GetAttributes()
if hook, ok := tr.openHooks[nt]; ok {
tag.WriteString(hook(n))
} else {
switch nt {
case NodeBlockquote:
tag.WriteString("> ")
case NodeCodeBlock:
tag.WriteString("```")
nl := true
if attrs != nil {
a := attrs.(map[string]interface{})
for k := range a {
if k == "language" {
nl = false
break
}
}
}
if nl {
tag.WriteString("\n")
}
case NodePanel:
tag.WriteString("---\n")
case NodeTable:
tag.WriteString("\n")
case NodeMedia:
tag.WriteString("\n[attachment]")
case NodeBulletList:
tr.list.depthU++
tr.list.ul[tr.list.depthU] = true
case NodeOrderedList:
tr.list.depthO++
tr.list.ol[tr.list.depthO] = true
case ChildNodeListItem:
if tr.list.ol[tr.list.depthO] {
for i := 0; i < tr.list.depthO-1; i++ {
tag.WriteString("\t")
}
tr.list.counter[tr.list.depthO]++
tag.WriteString(fmt.Sprintf("%d. ", tr.list.counter[tr.list.depthO]))
} else {
for i := 0; i < tr.list.depthU-1; i++ {
tag.WriteString("\t")
}
tag.WriteString("- ")
}
case ChildNodeTableHeader:
if tr.table.cols != 0 {
tag.WriteString(" | ")
}
tr.table.cols++
case ChildNodeTableCell:
if tr.table.ccol != 0 {
tag.WriteString(" | ")
}
tr.table.ccol++
case ChildNodeTableRow:
tr.table.rows++
if tr.table.rows == 1 && !tr.table.sep {
tr.table.sep = true
}
tr.table.ccol = 0
case InlineNodeHardBreak:
tag.WriteString("\n\n")
case InlineNodeMention:
tag.WriteString(" @")
case InlineNodeCard:
tag.WriteString(" 📍 ")
case MarkStrong:
tag.WriteString(" **")
case MarkEm:
tag.WriteString(" _")
case MarkCode:
tag.WriteString(" `")
case MarkStrike:
tag.WriteString(" -")
case MarkLink:
tag.WriteString(" [")
}
}
tag.WriteString(tr.setOpenTagAttributes(attrs))
return tag.String()
}
// Close implements TagCloser interface.
//
//nolint:gocyclo
func (tr *MarkdownTranslator) Close(n Connector) string {
var tag strings.Builder
nt := n.GetType()
if hook, ok := tr.closeHooks[nt]; ok {
tag.WriteString(hook(n))
} else {
switch nt {
case NodeBlockquote:
tag.WriteString("\n")
case NodeCodeBlock:
tag.WriteString("\n```\n")
case NodePanel:
tag.WriteString("---\n")
case NodeHeading:
tag.WriteString("\n")
case NodeBulletList:
tr.list.ul[tr.list.depthU] = false
tr.list.depthU--
case NodeOrderedList:
tr.list.ol[tr.list.depthO] = false
tr.list.depthO--
case NodeParagraph:
if tr.list.ul[tr.list.depthU] || tr.list.ol[tr.list.depthO] {
tag.WriteString("\n")
} else if tr.table.rows == 0 {
tag.WriteString("\n\n")
}
case NodeTable:
tr.table.rows = 0
tr.table.cols = 0
tr.table.sep = false
case ChildNodeTableRow:
tag.WriteString("\n")
if tr.table.sep {
for i := 0; i < tr.table.cols; i++ {
tag.WriteString("---")
if i != tr.table.cols-1 {
tag.WriteString(" | ")
}
}
tr.table.sep = false
tag.WriteString("\n")
}
case InlineNodeMention:
tag.WriteString(" ")
case InlineNodeEmoji:
tag.WriteString(" ")
case MarkStrong:
tag.WriteString("** ")
case MarkEm:
tag.WriteString("_ ")
case MarkCode:
tag.WriteString("` ")
case MarkStrike:
tag.WriteString("- ")
case MarkLink:
tag.WriteString("]")
}
}
tag.WriteString(tr.setCloseTagAttributes(n.GetAttributes()))
return tag.String()
}
func (tr *MarkdownTranslator) setOpenTagAttributes(a interface{}) string {
if a == nil {
return ""
}
var (
tag strings.Builder
nl bool
)
attrs := a.(map[string]interface{})
for k, v := range attrs {
if tr.isValidAttr(k) {
switch k {
case "language":
tag.WriteString(fmt.Sprintf("%s", v))
nl = true
case "level":
for i := 0; i < int(v.(float64)); i++ {
tag.WriteString("#")
}
tag.WriteString(" ")
case "text":
tag.WriteString(fmt.Sprintf("%s", v))
nl = false
}
}
if nl {
tag.WriteString("\n")
}
}
return tag.String()
}
func (*MarkdownTranslator) setCloseTagAttributes(a interface{}) string {
if a == nil {
return ""
}
var tag strings.Builder
attrs := a.(map[string]interface{})
if h, ok := attrs["href"]; ok {
tag.WriteString(fmt.Sprintf("(%s) ", h))
} else if h, ok := attrs["url"]; ok {
tag.WriteString(fmt.Sprintf("%s ", h))
}
return tag.String()
}
func (*MarkdownTranslator) isValidAttr(attr string) bool {
known := []string{"language", "level", "text"}
for _, k := range known {
if k == attr {
return true
}
}
return false
}