package markdown import ( "regexp" "strconv" "strings" ) const ( blockSeparator = "\n\n" hardBreak = "\\\n" bulletMarker = "- " codeFence = "```" alignOpen = `
` alignClose = "
" ) var textEscaper = strings.NewReplacer( "\\", "\\\\", "*", "\\*", "_", "\\_", "`", "\\`", "[", "\\[", "]", "\\]", "<", "\\<", ) var ( numberedLineStart = regexp.MustCompile(`^(\d+)([.)])(\s|$)`) markerLineStart = regexp.MustCompile(`^(>|#{1,6}(\s|$)|[+-](\s|$|-)|=+\s*$)`) ) var hrefEscaper = strings.NewReplacer(" ", "%20", "(", "%28", ")", "%29", "<", "%3C", ">", "%3E") func renderMarkdown(blocks []block) string { return renderBlocks(blocks, blockSeparator) } func renderBlocks(blocks []block, separator string) string { parts := make([]string, 0, len(blocks)) for _, b := range blocks { if rendered := renderBlock(b); rendered != "" { parts = append(parts, rendered) } } return strings.Join(parts, separator) } func renderBlock(b block) string { rendered := renderPlainBlock(b) if b.align == "" || rendered == "" { return rendered } return alignOpen + b.align + alignOpenEnd + blockSeparator + rendered + blockSeparator + alignClose } func renderPlainBlock(b block) string { switch b.kind { case kindHeading: return strings.Repeat("#", b.level) + " " + renderInlines(b.inlines) case kindParagraph: return escapeLineStarts(renderInlines(b.inlines)) case kindList: return renderList(b) case kindBlockquote: return prefixLines(renderBlocks(b.children, blockSeparator), "> ", ">") case kindCode: return renderCode(b) case kindImage: return "![" + escapeAlt(b.alt) + "](" + hrefEscaper.Replace(b.src) + ")" } return "---" } func escapeLineStarts(text string) string { lines := strings.Split(text, "\n") for index, line := range lines { line = numberedLineStart.ReplaceAllString(line, `$1\$2$3`) if markerLineStart.MatchString(line) { line = `\` + line } lines[index] = line } return strings.Join(lines, "\n") } func renderCode(b block) string { fence := codeFence for strings.Contains(b.text, fence) { fence += "`" } return fence + b.language + "\n" + b.text + "\n" + fence } func escapeAlt(alt string) string { return strings.NewReplacer("[", "\\[", "]", "\\]", "\n", " ").Replace(alt) } func renderList(b block) string { lines := make([]string, 0, len(b.items)) for index, item := range b.items { marker := bulletMarker if b.ordered { marker = strconv.Itoa(index+1) + ". " } content := renderBlocks(item, itemSeparator(item)) lines = append(lines, marker+indentContinuation(content, len(marker))) } return strings.Join(lines, "\n") } func itemSeparator(item []block) string { textBlocks := 0 for _, b := range item { if b.kind != kindList { textBlocks++ } } if textBlocks > 1 { return blockSeparator } return "\n" } func indentContinuation(content string, width int) string { indent := strings.Repeat(" ", width) lines := strings.Split(content, "\n") for index := 1; index < len(lines); index++ { if lines[index] != "" { lines[index] = indent + lines[index] } } return strings.Join(lines, "\n") } func prefixLines(content, prefix, emptyPrefix string) string { lines := strings.Split(content, "\n") for index, line := range lines { if line == "" { lines[index] = emptyPrefix continue } lines[index] = prefix + line } return strings.Join(lines, "\n") } func renderInlines(inlines []inline) string { var builder strings.Builder for _, item := range inlines { builder.WriteString(renderInline(item)) } return builder.String() } func renderInline(item inline) string { isCode := hasMarkKind(item.marks, markCode) lines := strings.Split(item.text, "\n") for index, line := range lines { lines[index] = wrapMarks(item.marks, line, isCode) } return strings.Join(lines, hardBreak) } func hasMarkKind(marks []mark, kind string) bool { for _, m := range marks { if m.kind == kind { return true } } return false } func wrapMarks(marks []mark, text string, isCode bool) string { core := strings.TrimSpace(text) if core == "" { return text } start := strings.Index(text, core) leading, trailing := text[:start], text[start+len(core):] var wrapped string if isCode { wrapped = inlineCode(core) } else { wrapped = escapeText(core) } for index := len(marks) - 1; index >= 0; index-- { wrapped = applyMark(marks[index], wrapped) } return leading + wrapped + trailing } func inlineCode(text string) string { fence := "`" for strings.Contains(text, fence) { fence += "`" } if strings.HasPrefix(text, "`") || strings.HasSuffix(text, "`") { return fence + " " + text + " " + fence } return fence + text + fence } func applyMark(m mark, text string) string { switch m.kind { case markBold: return "**" + text + "**" case markItalic: return "*" + text + "*" case markLink: return "[" + text + "](" + hrefEscaper.Replace(m.href) + ")" } return text } func escapeText(text string) string { return textEscaper.Replace(text) }