jira-cli/pkg/adf/adf.go

252 lines
5.5 KiB
Go

package adf
import (
"strings"
)
// NodeType is an Atlassian document node type.
type NodeType string
// Node types.
const (
NodeTypeParent = NodeType("parent")
NodeTypeChild = NodeType("child")
NodeTypeUnknown = NodeType("unknown")
NodeBlockquote = NodeType("blockquote")
NodeBulletList = NodeType("bulletList")
NodeCodeBlock = NodeType("codeBlock")
NodeHeading = NodeType("heading")
NodeOrderedList = NodeType("orderedList")
NodePanel = NodeType("panel")
NodeParagraph = NodeType("paragraph")
NodeTable = NodeType("table")
NodeMedia = NodeType("media")
ChildNodeText = NodeType("text")
ChildNodeListItem = NodeType("listItem")
ChildNodeTableRow = NodeType("tableRow")
ChildNodeTableHeader = NodeType("tableHeader")
ChildNodeTableCell = NodeType("tableCell")
InlineNodeCard = NodeType("inlineCard")
InlineNodeEmoji = NodeType("emoji")
InlineNodeMention = NodeType("mention")
InlineNodeHardBreak = NodeType("hardBreak")
MarkEm = NodeType("em")
MarkLink = NodeType("link")
MarkCode = NodeType("code")
MarkStrike = NodeType("strike")
MarkStrong = NodeType("strong")
)
// TagOpener is a tag opener.
type TagOpener interface {
Open(c Connector, depth int) string
}
// TagCloser is a tag closer.
type TagCloser interface {
Close(Connector) string
}
// TagOpenerCloser wraps tag opener and closer.
type TagOpenerCloser interface {
TagOpener
TagCloser
}
// Connector is a connector interface.
type Connector interface {
GetType() NodeType
GetAttributes() interface{}
}
// ADF is an Atlassian document format object.
type ADF struct {
Version int `json:"version"`
DocType string `json:"type"`
Content []*Node `json:"content"`
}
// ReplaceAll replaces all occurrences of an old string
// in a text node with a new one.
func (a *ADF) ReplaceAll(old, new string) {
if a == nil || len(a.Content) == 0 {
return
}
for _, parent := range a.Content {
a.replace(parent, old, new)
}
}
func (a *ADF) replace(n *Node, old, new string) {
for _, child := range n.Content {
a.replace(child, old, new)
}
if n.NodeType == ChildNodeText {
n.Text = strings.ReplaceAll(n.Text, old, new)
}
}
// Node is an ADF content node.
type Node struct {
NodeType NodeType `json:"type"`
Content []*Node `json:"content,omitempty"`
Attributes interface{} `json:"attrs,omitempty"`
NodeValue
}
// GetType gets node type.
func (n Node) GetType() NodeType { return n.NodeType }
// GetAttributes gets node attributes.
func (n Node) GetAttributes() interface{} { return n.Attributes }
// NodeValue is an actual ADF node content.
type NodeValue struct {
Text string `json:"text,omitempty"`
Marks []MarkNode `json:"marks,omitempty"`
}
// MarkNode is a mark node type.
type MarkNode struct {
MarkType NodeType `json:"type,omitempty"`
Attributes interface{} `json:"attrs,omitempty"`
}
// GetType gets node type.
func (n MarkNode) GetType() NodeType { return n.MarkType }
// GetAttributes gets node attributes.
func (n MarkNode) GetAttributes() interface{} { return n.Attributes }
// ParentNodes returns supported ADF parent nodes.
func ParentNodes() []NodeType {
return []NodeType{
NodeBlockquote,
NodeBulletList,
NodeCodeBlock,
NodeHeading,
NodeOrderedList,
NodePanel,
NodeParagraph,
NodeTable,
NodeMedia,
}
}
// ChildNodes returns supported ADF child nodes.
func ChildNodes() []NodeType {
return []NodeType{
ChildNodeText,
ChildNodeListItem,
ChildNodeTableRow,
ChildNodeTableHeader,
ChildNodeTableCell,
}
}
// IsParentNode checks if the node is a parent node.
func IsParentNode(identifier NodeType) bool {
for _, n := range ParentNodes() {
if n == identifier {
return true
}
}
return false
}
// IsChildNode checks if the node is a child node.
func IsChildNode(identifier NodeType) bool {
for _, n := range ChildNodes() {
if n == identifier {
return true
}
}
return false
}
// GetADFNodeType returns the type of ADF node.
func GetADFNodeType(identifier NodeType) NodeType {
if IsParentNode(identifier) {
return NodeTypeParent
}
if IsChildNode(identifier) {
return NodeTypeChild
}
return NodeTypeUnknown
}
// Translator transforms ADF to a new format.
type Translator struct {
doc *ADF
tsl TagOpenerCloser
buf *strings.Builder
}
// NewTranslator constructs an ADF translator.
func NewTranslator(adf *ADF, tr TagOpenerCloser) *Translator {
return &Translator{
doc: adf,
tsl: tr,
buf: new(strings.Builder),
}
}
// Translate translates ADF to a new format.
func (a *Translator) Translate() string {
a.walk()
return a.buf.String()
}
func (a *Translator) walk() {
if a.doc == nil || len(a.doc.Content) == 0 {
return
}
for _, parent := range a.doc.Content {
a.visit(parent, 0)
}
}
func (a *Translator) visit(n *Node, depth int) {
a.buf.WriteString(a.tsl.Open(n, depth))
for _, child := range n.Content {
a.visit(child, depth+1)
}
if GetADFNodeType(n.NodeType) == NodeTypeChild {
var tag strings.Builder
opened := make([]MarkNode, 0, len(n.Marks))
if n.NodeType == ChildNodeText {
for _, m := range n.Marks {
opened = append(opened, m)
tag.WriteString(a.tsl.Open(m, depth))
}
}
tag.WriteString(sanitize(n.Text))
// Close tags in reverse order.
for i := len(opened) - 1; i >= 0; i-- {
m := opened[i]
tag.WriteString(a.tsl.Close(m))
}
a.buf.WriteString(tag.String())
}
a.buf.WriteString(a.tsl.Close(n))
}
func sanitize(s string) string {
s = strings.TrimSpace(s)
s = strings.TrimRight(s, "\n")
s = strings.ReplaceAll(s, "<", "❬")
s = strings.ReplaceAll(s, ">", "❭")
return s
}