package markdown import ( "html" "strings" "unsafe" ) var ( headerStart = []string{"
") } } else if newQuoteLevel < r.quoteLevel { r.closeParagraphs() for range r.quoteLevel - newQuoteLevel { r.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.WriteString(headerStart[space-1]) r.writeText(line[space+1:]) r.WriteString(headerEnd[space-1]) } return case '-', '*': if strings.HasPrefix(line, "---") { r.WriteString("
")
} else {
r.WriteString("")
}
r.inCodeBlock = true
}
return
}
case '|':
line = line[1:]
if r.tableLevel == 0 {
r.WriteString("")
r.tableLevel++
}
column := 0
for {
pipe := strings.IndexByte(line, '|')
if pipe == -1 {
r.WriteString("")
return
}
content := strings.TrimSpace(line[:pipe])
if strings.HasPrefix(content, "---") {
r.WriteString("")
r.tableHeaderWritten = true
return
}
if column == 0 {
r.WriteString("")
}
if r.tableHeaderWritten {
r.WriteString("")
r.writeText(content)
r.WriteString(" ")
} else {
r.WriteString("")
r.writeText(content)
r.WriteString(" ")
}
line = line[pipe+1:]
column++
}
}
if r.paragraphLevel == 0 {
r.WriteString("")
r.paragraphLevel++
r.writeText(line)
return
}
r.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.WriteString("
")
}
r.paragraphLevel = 0
}
// closeLists closes open lists.
func (r *renderer) closeLists() {
for range r.listLevel {
r.WriteString("")
}
r.listLevel = 0
}
// closeTables closes open tables.
func (r *renderer) closeTables() {
for range r.tableLevel {
r.WriteString("
")
}
r.tableLevel = 0
r.tableHeaderWritten = false
}
// writeText converts inline markdown to HTML.
func (r *renderer) writeText(markdown string) {
var (
tokenStart = 0
searchStart = 0
linkTextStart = -1
linkTextEnd = -1
urlStart = -1
codeStart = -1
emStart = -1
strongStart = -1
parentheses = 0
)
for {
i := strings.IndexAny(markdown[searchStart:], "[]()`*_")
if i == -1 {
r.WriteString(html.EscapeString(markdown[tokenStart:]))
return
}
i += searchStart
searchStart = i + 1
c := markdown[i]
switch c {
case '[':
r.WriteString(html.EscapeString(markdown[tokenStart:i]))
tokenStart = i
linkTextStart = i
case ']':
linkTextEnd = i
case '(':
if parentheses == 0 {
urlStart = i
}
parentheses++
case ')':
parentheses--
if parentheses == 0 && linkTextStart >= 0 && linkTextEnd >= 0 && urlStart >= 0 {
linkText := markdown[linkTextStart+1 : linkTextEnd]
linkURL := markdown[urlStart+1 : i]
r.WriteString("")
r.WriteString(html.EscapeString(linkText))
r.WriteString("")
linkTextStart = -1
linkTextEnd = -1
urlStart = -1
tokenStart = i + 1
}
case '`':
if codeStart != -1 {
r.WriteString("")
r.WriteString(html.EscapeString(markdown[codeStart:i]))
r.WriteString("
")
codeStart = -1
tokenStart = i + 1
} else {
r.WriteString(html.EscapeString(markdown[tokenStart:i]))
tokenStart = i
codeStart = i + 1
}
case '*', '_':
if i == emStart {
strongStart = i + 1
emStart = -1
} else if strongStart != -1 {
r.WriteString("")
r.WriteString(html.EscapeString(markdown[strongStart:i]))
r.WriteString("")
strongStart = -1
tokenStart = i + 2
searchStart = tokenStart
} else if emStart != -1 {
r.WriteString("")
r.WriteString(html.EscapeString(markdown[emStart:i]))
r.WriteString("")
emStart = -1
tokenStart = i + 1
} else {
r.WriteString(html.EscapeString(markdown[tokenStart:i]))
tokenStart = i
emStart = i + 1
}
}
}
}
// 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)
}
// WriteByte adds a single byte to the output.
func (r *renderer) WriteByte(b byte) error {
r.out = append(r.out, b)
return nil
}
// WriteString adds a string to the output.
func (r *renderer) WriteString(text string) (int, error) {
r.out = append(r.out, text...)
return len(text), nil
}
// nextPowerOf2 calculates the next 32-bit power of 2.
func nextPowerOf2(x uint32) uint32 {
x--
x |= x >> 1
x |= x >> 2
x |= x >> 4
x |= x >> 8
x |= x >> 16
x++
return x
}