// Copyright 2015 The Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package markdown
import (
"io"
"regexp"
"strconv"
"strings"
"gitlab.com/golang-commonmark/html"
)
type Renderer struct {
w *monadicWriter
}
func NewRenderer(w io.Writer) *Renderer {
return &Renderer{newMonadicWriter(w)}
}
func (r *Renderer) Render(tokens []Token, options RenderOptions) error {
for i, tok := range tokens {
if tok, ok := tok.(*Inline); ok {
r.renderInline(tok.Children, options)
} else {
r.renderToken(tokens, i, options)
}
}
r.w.Flush()
return r.w.err
}
func (r *Renderer) renderInline(tokens []Token, o RenderOptions) {
for i := range tokens {
r.renderToken(tokens, i, o)
}
}
func (r *Renderer) renderInlineAsText(tokens []Token) {
for _, tok := range tokens {
if text, ok := tok.(*Text); ok {
html.WriteEscapedString(r.w, text.Content)
} else if img, ok := tok.(*Image); ok {
r.renderInlineAsText(img.Tokens)
}
}
}
var rNotSpace = regexp.MustCompile(`^\S+`)
func (r *Renderer) renderToken(tokens []Token, idx int, options RenderOptions) {
tok := tokens[idx]
if idx > 0 && tok.Block() && !tok.Closing() {
switch t := tokens[idx-1].(type) {
case *ParagraphOpen:
if t.Hidden {
r.w.WriteByte('\n')
}
case *ParagraphClose:
if t.Hidden {
r.w.WriteByte('\n')
}
}
}
switch tok := tok.(type) {
case *BlockquoteClose:
r.w.WriteString("</blockquote>")
case *BlockquoteOpen:
r.w.WriteString("<blockquote>")
case *BulletListClose:
r.w.WriteString("</ul>")
case *BulletListOpen:
r.w.WriteString("<ul>")
case *CodeBlock:
r.w.WriteString("<pre><code>")
html.WriteEscapedString(r.w, tok.Content)
r.w.WriteString("</code></pre>")
case *CodeInline:
r.w.WriteString("<code>")
html.WriteEscapedString(r.w, tok.Content)
r.w.WriteString("</code>")
case *EmphasisClose:
r.w.WriteString("</em>")
case *EmphasisOpen:
r.w.WriteString("<em>")
case *Fence:
r.w.WriteString("<pre><code")
if tok.Params != "" {
langName := strings.SplitN(unescapeAll(tok.Params), " ", 2)[0]
langName = rNotSpace.FindString(langName)
if langName != "" {
r.w.WriteString(` class="`)
r.w.WriteString(options.LangPrefix)
html.WriteEscapedString(r.w, langName)
r.w.WriteByte('"')
}
}
r.w.WriteByte('>')
html.WriteEscapedString(r.w, tok.Content)
r.w.WriteString("</code></pre>")
case *Hardbreak:
if options.XHTML {
r.w.WriteString("<br />\n")
} else {
r.w.WriteString("<br>\n")
}
case *HeadingClose:
r.w.WriteString("</h")
r.w.WriteByte("0123456789"[tok.HLevel])
r.w.WriteString(">")
case *HeadingOpen:
r.w.WriteString("<h")
r.w.WriteByte("0123456789"[tok.HLevel])
r.w.WriteByte('>')
case *Hr:
if options.XHTML {
r.w.WriteString("<hr />")
} else {
r.w.WriteString("<hr>")
}
case *HTMLBlock:
r.w.WriteString(tok.Content)
return // no newline
case *HTMLInline:
r.w.WriteString(tok.Content)
case *Image:
r.w.WriteString(`<img src="`)
html.WriteEscapedString(r.w, tok.Src)
r.w.WriteString(`" alt="`)
r.renderInlineAsText(tok.Tokens)
r.w.WriteByte('"')
if tok.Title != "" {
r.w.WriteString(` title="`)
html.WriteEscapedString(r.w, tok.Title)
r.w.WriteByte('"')
}
if options.XHTML {
r.w.WriteString(" />")
} else {
r.w.WriteByte('>')
}
case *LinkClose:
r.w.WriteString("</a>")
case *LinkOpen:
r.w.WriteString(`<a href="`)
html.WriteEscapedString(r.w, tok.Href)
r.w.WriteByte('"')
if tok.Title != "" {
r.w.WriteString(` title="`)
html.WriteEscapedString(r.w, (tok.Title))
r.w.WriteByte('"')
}
if tok.Target != "" {
r.w.WriteString(` target="`)
html.WriteEscapedString(r.w, tok.Target)
r.w.WriteByte('"')
}
if options.Nofollow {
r.w.WriteString(` rel="nofollow"`)
}
r.w.WriteByte('>')
case *ListItemClose:
r.w.WriteString("</li>")
case *ListItemOpen:
r.w.WriteString("<li>")
case *OrderedListClose:
r.w.WriteString("</ol>")
case *OrderedListOpen:
if tok.Order != 1 {
r.w.WriteString(`<ol start="`)
r.w.WriteString(strconv.Itoa(tok.Order))
r.w.WriteString(`">`)
} else {
r.w.WriteString("<ol>")
}
case *ParagraphClose:
if tok.Hidden {
return
}
if !tok.Tight {
r.w.WriteString("</p>")
} else if tokens[idx+1].Closing() {
return // no newline
}
case *ParagraphOpen:
if tok.Hidden {
return
}
if !tok.Tight {
r.w.WriteString("<p>")
}
case *Softbreak:
if options.Breaks {
if options.XHTML {
r.w.WriteString("<br />\n")
} else {
r.w.WriteString("<br>\n")
}
} else {
r.w.WriteByte('\n')
}
return
case *StrongClose:
r.w.WriteString("</strong>")
case *StrongOpen:
r.w.WriteString("<strong>")
case *StrikethroughClose:
r.w.WriteString("</s>")
case *StrikethroughOpen:
r.w.WriteString("<s>")
case *TableClose:
r.w.WriteString("</table>")
case *TableOpen:
r.w.WriteString("<table>")
case *TbodyClose:
r.w.WriteString("</tbody>")
case *TbodyOpen:
r.w.WriteString("<tbody>")
case *TdClose:
r.w.WriteString("</td>")
case *TdOpen:
if tok.Align != AlignNone {
r.w.WriteString(`<td style="text-align:`)
r.w.WriteString(tok.Align.String())
r.w.WriteString(`">`)
} else {
r.w.WriteString("<td>")
}
case *Text:
html.WriteEscapedString(r.w, tok.Content)
case *TheadClose:
r.w.WriteString("</thead>")
case *TheadOpen:
r.w.WriteString("<thead>")
case *ThClose:
r.w.WriteString("</th>")
case *ThOpen:
if align := tok.Align; align != AlignNone {
r.w.WriteString(`<th style="text-align:`)
r.w.WriteString(align.String())
r.w.WriteString(`">`)
} else {
r.w.WriteString("<th>")
}
case *TrClose:
r.w.WriteString("</tr>")
case *TrOpen:
r.w.WriteString("<tr>")
default:
panic("unknown token type")
}
needLf := false
if tok.Block() {
needLf = true
if tok.Opening() {
nextTok := tokens[idx+1]
blockquote := false
switch nextTok := nextTok.(type) {
case *Inline:
needLf = false
case *ParagraphOpen:
if nextTok.Tight || nextTok.Hidden {
needLf = false
}
case *ParagraphClose:
if nextTok.Tight || nextTok.Hidden {
needLf = false
}
case *BlockquoteClose:
blockquote = true
}
if !blockquote && needLf && nextTok.Closing() && nextTok.Tag() == tok.Tag() {
needLf = false
}
}
}
if needLf {
r.w.WriteByte('\n')
}
}