feat: articles format option with markdown new default
This commit is contained in:
1 parent
68401d66e0
commit
7e9bd9ba30
61 files changed
+2269
-243
No files matched your search
@@ -0,0 +1,213 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
blockSeparator = "\n\n"
|
||||
hardBreak = "\\\n"
|
||||
bulletMarker = "- "
|
||||
codeFence = "```"
|
||||
alignOpen = `<div style="text-align: `
|
||||
alignOpenEnd = `">`
|
||||
alignClose = "</div>"
|
||||
)
|
||||
|
||||
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 " + ")"
|
||||
}
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user