package markdown import ( "html" "strings" ) var ( headerStart = []string{"

", "

", "

", "

", "

", "
"} headerEnd = []string{"
", "", "", "", "", ""} ) type renderer struct { out strings.Builder paragraphLevel int quoteLevel int } // 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.closeParagraphs() 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 strings.HasPrefix(line, "#") { r.closeParagraphs() 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 } if len(line) == 0 { r.closeParagraphs() return } if r.paragraphLevel == 0 { r.out.WriteString("

") r.paragraphLevel++ r.writeText(line) return } r.out.WriteByte(' ') r.writeText(line) } // closeParagraphs closes open paragraphs. func (r *renderer) closeParagraphs() { for range r.paragraphLevel { r.out.WriteString("

") } r.paragraphLevel = 0 } // 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++ } } func sanitizeURL(linkURL string) string { linkURL = strings.TrimSpace(linkURL) if strings.HasPrefix(strings.ToLower(linkURL), "javascript:") { return "" } return html.EscapeString(linkURL) }