package mark import ( "fmt" "regexp" "strconv" "strings" ) // A Node is an element in the parse tree. type Node interface { Type() NodeType Render() string } // NodeType identifies the type of a parse tree node. type NodeType int // Type returns itself and provides an easy default implementation // for embedding in a Node. Embedded in all non-trivial Nodes. func (t NodeType) Type() NodeType { return t } // Render function, used for overriding default rendering. type RenderFn func(Node) string const ( NodeText NodeType = iota // A plain text NodeParagraph // A Paragraph NodeEmphasis // An emphasis(strong, em, ...) NodeHeading // A heading (h1, h2, ...) NodeBr // A link break NodeHr // A horizontal rule NodeImage // An image NodeRefImage // A image reference NodeList // A list of ListItems NodeListItem // A list item node NodeLink // A link(href) NodeRefLink // A link reference NodeDefLink // A link definition NodeTable // A table of NodeRows NodeRow // A row of NodeCells NodeCell // A table-cell(td) NodeCode // A code block(wrapped with pre) NodeBlockQuote // A blockquote NodeHTML // An inline HTML NodeCheckbox // A checkbox ) // ParagraphNode hold simple paragraph node contains text // that may be emphasis. type ParagraphNode struct { NodeType Pos Nodes []Node } // Render returns the html representation of ParagraphNode func (n *ParagraphNode) Render() (s string) { for _, node := range n.Nodes { s += node.Render() } return wrap("p", s) } func (p *parse) newParagraph(pos Pos) *ParagraphNode { return &ParagraphNode{NodeType: NodeParagraph, Pos: pos} } // TextNode holds plain text. type TextNode struct { NodeType Pos Text string } // Render returns the string representation of TexNode func (n *TextNode) Render() string { return n.Text } func (p *parse) newText(pos Pos, text string) *TextNode { return &TextNode{NodeType: NodeText, Pos: pos, Text: p.text(text)} } // HTMLNode holds the raw html source. type HTMLNode struct { NodeType Pos Src string } // Render returns the src of the HTMLNode func (n *HTMLNode) Render() string { return n.Src } func (p *parse) newHTML(pos Pos, src string) *HTMLNode { return &HTMLNode{NodeType: NodeHTML, Pos: pos, Src: src} } // HrNode represents horizontal rule type HrNode struct { NodeType Pos } // Render returns the html representation of hr. func (n *HrNode) Render() string { return "
" } func (p *parse) newHr(pos Pos) *HrNode { return &HrNode{NodeType: NodeHr, Pos: pos} } // BrNode represents a link-break element. type BrNode struct { NodeType Pos } // Render returns the html representation of line-break. func (n *BrNode) Render() string { return "
" } func (p *parse) newBr(pos Pos) *BrNode { return &BrNode{NodeType: NodeBr, Pos: pos} } // EmphasisNode holds plain-text wrapped with style. // (strong, em, del, code) type EmphasisNode struct { NodeType Pos Style itemType Nodes []Node } // Tag return the tagName based on the Style field. func (n *EmphasisNode) Tag() (s string) { switch n.Style { case itemStrong: s = "strong" case itemItalic: s = "em" case itemStrike: s = "del" case itemCode: s = "code" } return } // Return the html representation of emphasis text. func (n *EmphasisNode) Render() string { var s string for _, node := range n.Nodes { s += node.Render() } return wrap(n.Tag(), s) } func (p *parse) newEmphasis(pos Pos, style itemType) *EmphasisNode { return &EmphasisNode{NodeType: NodeEmphasis, Pos: pos, Style: style} } // HeadingNode holds heaing element with specific level(1-6). type HeadingNode struct { NodeType Pos Level int Text string Nodes []Node } // Render returns the html representation based on heading level. func (n *HeadingNode) Render() (s string) { for _, node := range n.Nodes { s += node.Render() } re := regexp.MustCompile(`[^\w]+`) id := re.ReplaceAllString(n.Text, "-") // ToLowerCase id = strings.ToLower(id) return fmt.Sprintf("<%[1]s id=\"%s\">%s", "h"+strconv.Itoa(n.Level), id, s) } func (p *parse) newHeading(pos Pos, level int, text string) *HeadingNode { return &HeadingNode{NodeType: NodeHeading, Pos: pos, Level: level, Text: p.text(text)} } // Code holds CodeBlock node with specific lang field. type CodeNode struct { NodeType Pos Lang, Text string } // Return the html representation of codeBlock func (n *CodeNode) Render() string { var attr string if n.Lang != "" { attr = fmt.Sprintf(" class=\"lang-%s\"", n.Lang) } code := fmt.Sprintf("<%[1]s%s>%s", "code", attr, n.Text) return wrap("pre", code) } func (p *parse) newCode(pos Pos, lang, text string) *CodeNode { // DRY: see `escape()` below text = strings.NewReplacer("<", "<", ">", ">", "\"", """, "&", "&").Replace(text) return &CodeNode{NodeType: NodeCode, Pos: pos, Lang: lang, Text: text} } // Link holds a tag with optional title type LinkNode struct { NodeType Pos Title, Href string Nodes []Node } // Return the html representation of link node func (n *LinkNode) Render() (s string) { for _, node := range n.Nodes { s += node.Render() } attrs := fmt.Sprintf("href=\"%s\"", n.Href) if n.Title != "" { attrs += fmt.Sprintf(" title=\"%s\"", n.Title) } return fmt.Sprintf("%s", attrs, s) } func (p *parse) newLink(pos Pos, title, href string, nodes ...Node) *LinkNode { return &LinkNode{NodeType: NodeLink, Pos: pos, Title: p.text(title), Href: p.text(href), Nodes: nodes} } // RefLink holds link with refrence to link definition type RefNode struct { NodeType Pos tr *parse Text, Ref, Raw string Nodes []Node } // rendering based type func (n *RefNode) Render() string { var node Node ref := strings.ToLower(n.Ref) if l, ok := n.tr.links[ref]; ok { if n.Type() == NodeRefLink { node = n.tr.newLink(n.Pos, l.Title, l.Href, n.Nodes...) } else { node = n.tr.newImage(n.Pos, l.Title, l.Href, n.Text) } } else { node = n.tr.newText(n.Pos, n.Raw) } return node.Render() } // newRefLink create new RefLink that suitable for link func (p *parse) newRefLink(typ itemType, pos Pos, raw, ref string, text []Node) *RefNode { return &RefNode{NodeType: NodeRefLink, Pos: pos, tr: p.root(), Raw: raw, Ref: ref, Nodes: text} } // newRefImage create new RefLink that suitable for image func (p *parse) newRefImage(typ itemType, pos Pos, raw, ref, text string) *RefNode { return &RefNode{NodeType: NodeRefImage, Pos: pos, tr: p.root(), Raw: raw, Ref: ref, Text: text} } // DefLinkNode refresent single reference to link-definition type DefLinkNode struct { NodeType Pos Name, Href, Title string } // Deflink have no representation(Transparent node) func (n *DefLinkNode) Render() string { return "" } func (p *parse) newDefLink(pos Pos, name, href, title string) *DefLinkNode { return &DefLinkNode{NodeType: NodeLink, Pos: pos, Name: name, Href: href, Title: title} } // ImageNode represents an image element with optional alt and title attributes. type ImageNode struct { NodeType Pos Title, Src, Alt string } // Render returns the html representation on image node func (n *ImageNode) Render() string { attrs := fmt.Sprintf("src=\"%s\" alt=\"%s\"", n.Src, n.Alt) if n.Title != "" { attrs += fmt.Sprintf(" title=\"%s\"", n.Title) } return fmt.Sprintf("", attrs) } func (p *parse) newImage(pos Pos, title, src, alt string) *ImageNode { return &ImageNode{NodeType: NodeImage, Pos: pos, Title: p.text(title), Src: p.text(src), Alt: p.text(alt)} } // ListNode holds list items nodes in ordered or unordered states. type ListNode struct { NodeType Pos Ordered bool Items []*ListItemNode } func (n *ListNode) append(item *ListItemNode) { n.Items = append(n.Items, item) } // Render returns the html representation of orderd(ol) or unordered(ul) list. func (n *ListNode) Render() (s string) { tag := "ul" if n.Ordered { tag = "ol" } for _, item := range n.Items { s += "\n" + item.Render() } s += "\n" return wrap(tag, s) } func (p *parse) newList(pos Pos, ordered bool) *ListNode { return &ListNode{NodeType: NodeList, Pos: pos, Ordered: ordered} } // ListItem represents single item in ListNode that may contains nested nodes. type ListItemNode struct { NodeType Pos Nodes []Node } func (l *ListItemNode) append(n Node) { l.Nodes = append(l.Nodes, n) } // Render returns the html representation of list-item func (l *ListItemNode) Render() (s string) { for _, node := range l.Nodes { s += node.Render() } return wrap("li", s) } func (p *parse) newListItem(pos Pos) *ListItemNode { return &ListItemNode{NodeType: NodeListItem, Pos: pos} } // TableNode represents table element contains head and body type TableNode struct { NodeType Pos Rows []*RowNode } func (n *TableNode) append(row *RowNode) { n.Rows = append(n.Rows, row) } // Render returns the html representation of a table func (n *TableNode) Render() string { var s string for i, row := range n.Rows { s += "\n" switch i { case 0: s += wrap("thead", "\n"+row.Render()+"\n") case 1: s += "\n" fallthrough default: s += row.Render() } } s += "\n\n" return wrap("table", s) } func (p *parse) newTable(pos Pos) *TableNode { return &TableNode{NodeType: NodeTable, Pos: pos} } // RowNode represnt tr that holds list of cell-nodes type RowNode struct { NodeType Pos Cells []*CellNode } func (r *RowNode) append(cell *CellNode) { r.Cells = append(r.Cells, cell) } // Render returns the html representation of table-row func (r *RowNode) Render() string { var s string for _, cell := range r.Cells { s += "\n" + cell.Render() } s += "\n" return wrap("tr", s) } func (p *parse) newRow(pos Pos) *RowNode { return &RowNode{NodeType: NodeRow, Pos: pos} } // AlignType identifies the aligment-type of specfic cell. type AlignType int // Align returns itself and provides an easy default implementation // for embedding in a Node. func (t AlignType) Align() AlignType { return t } // Alignment const ( None AlignType = iota Right Left Center ) // Cell types const ( Header = iota Data ) // CellNode represents table-data/cell that holds simple text(may be emphasis) // Note: the text in elements are bold and centered by default. type CellNode struct { NodeType Pos AlignType Kind int Nodes []Node } // Render returns the html reprenestation of table-cell func (c *CellNode) Render() string { var s string tag := "td" if c.Kind == Header { tag = "th" } for _, node := range c.Nodes { s += node.Render() } return fmt.Sprintf("<%[1]s%s>%s", tag, c.Style(), s) } // Style return the cell-style based on alignment field func (c *CellNode) Style() string { s := " style=\"text-align:" switch c.Align() { case Right: s += "right\"" case Left: s += "left\"" case Center: s += "center\"" default: s = "" } return s } func (p *parse) newCell(pos Pos, kind int, align AlignType) *CellNode { return &CellNode{NodeType: NodeCell, Pos: pos, Kind: kind, AlignType: align} } // BlockQuote represents block-quote tag. type BlockQuoteNode struct { NodeType Pos Nodes []Node } // Render returns the html representation of BlockQuote func (n *BlockQuoteNode) Render() string { var s string for _, node := range n.Nodes { s += node.Render() } return wrap("blockquote", s) } func (p *parse) newBlockQuote(pos Pos) *BlockQuoteNode { return &BlockQuoteNode{NodeType: NodeBlockQuote, Pos: pos} } // CheckboxNode represents checked and unchecked checkbox tag. // Used in task lists. type CheckboxNode struct { NodeType Pos Checked bool } // Render returns the html representation of checked and unchecked CheckBox. func (n *CheckboxNode) Render() string { s := "" } func (p *parse) newCheckbox(pos Pos, checked bool) *CheckboxNode { return &CheckboxNode{NodeType: NodeCheckbox, Pos: pos, Checked: checked} } // Wrap text with specific tag. func wrap(tag, body string) string { return fmt.Sprintf("<%[1]s>%s", tag, body) } // Group all text configuration in one place(escaping, smartypants, etc..) func (p *parse) text(input string) string { opts := p.root().options if opts.Smartypants { input = smartypants(input) } if opts.Fractions { input = smartyfractions(input) } return escape(input) } // Helper escaper func escape(str string) (cpy string) { emp := regexp.MustCompile(`&\w+;`) for i := 0; i < len(str); i++ { switch s := str[i]; s { case '>': cpy += ">" case '"': cpy += """ case '\'': cpy += "'" case '<': if res := reHTML.tag.FindString(str[i:]); res != "" { cpy += res i += len(res) - 1 } else { cpy += "<" } case '&': if res := emp.FindString(str[i:]); res != "" { cpy += res i += len(res) - 1 } else { cpy += "&" } default: cpy += str[i : i+1] } } return } // Smartypants transformation helper, translate from marked.js func smartypants(text string) string { // em-dashes, en-dashes, ellipses re := strings.NewReplacer("---", "\u2014", "--", "\u2013", "...", "\u2026") text = re.Replace(text) // opening singles text = regexp.MustCompile("(^|[-\u2014/(\\[{\"\\s])'").ReplaceAllString(text, "$1\u2018") // closing singles & apostrophes text = strings.Replace(text, "'", "\u2019", -1) // opening doubles text = regexp.MustCompile("(^|[-\u2014/(\\[{\u2018\\s])\"").ReplaceAllString(text, "$1\u201c") // closing doubles text = strings.Replace(text, "\"", "\u201d", -1) return text } // Smartyfractions transformation helper. func smartyfractions(text string) string { re := regexp.MustCompile(`(\d+)(/\d+)(/\d+|)`) return re.ReplaceAllStringFunc(text, func(str string) string { var match []string // If it's date like if match = re.FindStringSubmatch(str); match[3] != "" { return str } switch n := match[1] + match[2]; n { case "1/2", "1/3", "2/3", "1/4", "3/4", "1/5", "2/5", "3/5", "4/5", "1/6", "5/6", "1/7", "1/8", "3/8", "5/8", "7/8": return fmt.Sprintf("&frac%s;", strings.Replace(n, "/", "", 1)) default: return fmt.Sprintf("%s%s", match[1], strings.Replace(match[2], "/", "", 1)) } }) }