Files

118 lines
2.8 KiB
Go

// Package render turns Markdown into sanitized HTML for the public site.
package render
import (
"bytes"
"regexp"
"strings"
"unicode"
"unicode/utf8"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
)
var md = goldmark.New(
goldmark.WithExtensions(extension.GFM, extension.Footnote),
goldmark.WithParserOptions(parser.WithAutoHeadingID()),
goldmark.WithRendererOptions(html.WithHardWraps()),
)
var unsafeScheme = regexp.MustCompile(`(?i)(href|src)\s*=\s*"(javascript|data|vbscript):[^"]*"`)
// Markdown renders Markdown to HTML. Raw HTML stays escaped (goldmark default)
// and dangerous URL schemes are stripped.
func Markdown(src string) string {
var buf bytes.Buffer
if err := md.Convert([]byte(src), &buf); err != nil {
return "<p>" + escapeHTML(src) + "</p>"
}
out := buf.String()
out = unsafeScheme.ReplaceAllString(out, `$1="#"`)
return out
}
func escapeHTML(s string) string {
r := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;", `"`, "&quot;")
return r.Replace(s)
}
// ReadingMinutes estimates reading time: ~400 CJK chars or ~220 latin words
// per minute, whichever dominates.
func ReadingMinutes(markdown string) int {
if strings.TrimSpace(markdown) == "" {
return 1
}
cjk := 0
latinWords := 0
inWord := false
for _, r := range markdown {
switch {
case r >= 0x4E00 && r <= 0x9FFF, r >= 0x3400 && r <= 0x4DBF,
r >= 0x3000 && r <= 0x303F, r >= 0xFF00 && r <= 0xFFEF:
cjk++
inWord = false
case unicode.IsSpace(r):
inWord = false
default:
if !inWord {
latinWords++
inWord = true
}
}
}
minutes := cjk/400 + latinWords/220
if minutes < 1 {
return 1
}
return minutes
}
// Excerpt builds a plain-text summary from Markdown when the author left the
// summary field empty.
func Excerpt(markdown string, limit int) string {
var b strings.Builder
inFence := false
for _, line := range strings.Split(markdown, "\n") {
t := strings.TrimSpace(line)
if strings.HasPrefix(t, "```") {
inFence = !inFence
continue
}
if inFence || t == "" || strings.HasPrefix(t, "#") || strings.HasPrefix(t, ">") {
continue
}
t = strings.TrimLeft(t, "-*+0123456789. ")
b.WriteString(t)
b.WriteString(" ")
}
s := strings.TrimSpace(b.String())
if limit <= 0 {
limit = 140
}
runes := []rune(s)
if len(runes) <= limit {
return string(runes)
}
return string(runes[:limit]) + "…"
}
// TitleFromMarkdown derives a fallback title for short posts.
func TitleFromMarkdown(markdown string) string {
for _, line := range strings.Split(markdown, "\n") {
t := strings.TrimSpace(strings.TrimLeft(line, "# "))
if t != "" {
runes := []rune(t)
if len(runes) > 24 {
return string(runes[:24]) + "…"
}
return t
}
}
return "无题"
}
func RuneLen(s string) int { return utf8.RuneCountInString(s) }