package markdown
import (
"html"
"strings"
)
var (
headerStart = []string{"
", "", "", "", "", ""}
headerEnd = []string{"
", "", "", "", "", ""}
)
type renderer struct {
out strings.Builder
paragraphLevel int
quoteLevel int
listLevel int
tableLevel int
tableHeaderWritten bool
}
// Render creates HTML from the supplied markdown text.
func Render(markdown string) string {
var (
r renderer
i = 0
lineStart = 0
)
for {
if i > len(markdown) {
r.closeAll()
for range r.quoteLevel {
r.out.WriteString("")
}
return r.out.String()
}
if i != len(markdown) && markdown[i] != '\n' {
i++
continue
}
line := markdown[lineStart:i]
lineStart = i + 1
i++
r.processLine(line)
}
}
func (r *renderer) processLine(line string) {
newQuoteLevel := 0
for strings.HasPrefix(line, ">") {
line = strings.TrimSpace(line[1:])
newQuoteLevel++
}
if newQuoteLevel > r.quoteLevel {
r.closeParagraphs()
for range newQuoteLevel - r.quoteLevel {
r.out.WriteString("")
}
} else if newQuoteLevel < r.quoteLevel {
r.closeParagraphs()
for range r.quoteLevel - newQuoteLevel {
r.out.WriteString("
")
}
}
r.quoteLevel = newQuoteLevel
if len(line) == 0 {
r.closeAll()
return
}
switch line[0] {
case '#':
r.closeAll()
space := strings.IndexByte(line, ' ')
if space > 0 && space <= 6 {
r.out.WriteString(headerStart[space-1])
r.writeText(line[space+1:])
r.out.WriteString(headerEnd[space-1])
}
return
case '-', '*':
r.closeParagraphs()
line = strings.TrimSpace(line[1:])
if r.listLevel == 0 {
r.out.WriteString("")
r.listLevel++
}
r.out.WriteString("- ")
r.writeText(line)
r.out.WriteString("
")
return
case '|':
r.closeParagraphs()
line = line[1:]
if r.tableLevel == 0 {
r.out.WriteString("")
r.tableLevel++
}
column := 0
for {
pipe := strings.IndexByte(line, '|')
if pipe == -1 {
r.out.WriteString("")
return
}
content := strings.TrimSpace(line[:pipe])
if strings.HasPrefix(content, "---") {
r.out.WriteString("")
r.tableHeaderWritten = true
return
}
if column == 0 {
r.out.WriteString("")
}
if r.tableHeaderWritten {
r.out.WriteString("")
r.writeText(content)
r.out.WriteString(" | ")
} else {
r.out.WriteString("")
r.writeText(content)
r.out.WriteString(" | ")
}
line = line[pipe+1:]
column++
}
}
if r.paragraphLevel == 0 {
r.out.WriteString("")
r.paragraphLevel++
r.writeText(line)
return
}
r.out.WriteByte(' ')
r.writeText(line)
}
// closeAll closes all open tags.
func (r *renderer) closeAll() {
r.closeLists()
r.closeParagraphs()
r.closeTables()
}
// closeParagraphs closes open paragraphs.
func (r *renderer) closeParagraphs() {
for range r.paragraphLevel {
r.out.WriteString("
")
}
r.paragraphLevel = 0
}
// closeLists closes open lists.
func (r *renderer) closeLists() {
for range r.listLevel {
r.out.WriteString("")
}
r.listLevel = 0
}
// closeTables closes open tables.
func (r *renderer) closeTables() {
for range r.tableLevel {
r.out.WriteString("
")
}
r.tableLevel = 0
r.tableHeaderWritten = false
}
// writeText converts inline markdown to HTML.
func (r *renderer) writeText(markdown string) {
var (
i = 0
tokenStart = 0
)
var (
textStart = -1
textEnd = -1
urlStart = -1
parentheses = 0
)
for {
if i == len(markdown) {
r.out.WriteString(html.EscapeString(markdown[tokenStart:]))
return
}
c := markdown[i]
switch c {
case '[':
r.out.WriteString(html.EscapeString(markdown[tokenStart:i]))
tokenStart = i
textStart = i
case ']':
textEnd = i
case '(':
if parentheses == 0 {
urlStart = i
}
parentheses++
case ')':
parentheses--
if parentheses == 0 && textStart >= 0 && textEnd >= 0 && urlStart >= 0 {
linkText := markdown[textStart+1 : textEnd]
linkURL := markdown[urlStart+1 : i]
r.out.WriteString("")
r.out.WriteString(html.EscapeString(linkText))
r.out.WriteString("")
textStart = -1
textEnd = -1
urlStart = -1
tokenStart = i + 1
}
}
i++
}
}
// sanitizeURL makes a URL safe to use as the value for a `href` attribute.
func sanitizeURL(linkURL string) string {
linkURL = strings.TrimSpace(linkURL)
if strings.HasPrefix(strings.ToLower(linkURL), "javascript:") {
return ""
}
return html.EscapeString(linkURL)
}