169 lines
3.6 KiB
Go
169 lines
3.6 KiB
Go
package markdown
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/yuin/goldmark"
|
|
"github.com/yuin/goldmark/extension"
|
|
"github.com/yuin/goldmark/parser"
|
|
"github.com/yuin/goldmark/renderer/html"
|
|
)
|
|
|
|
var (
|
|
tagPattern = regexp.MustCompile(`(^|[\s(])#([a-zA-Z0-9_-]+)\b`)
|
|
)
|
|
|
|
type Result struct {
|
|
HTML template.HTML
|
|
Title string
|
|
Tags []string
|
|
}
|
|
|
|
type Renderer struct {
|
|
markdown goldmark.Markdown
|
|
}
|
|
|
|
func NewRenderer() *Renderer {
|
|
return &Renderer{
|
|
markdown: goldmark.New(
|
|
goldmark.WithExtensions(
|
|
extension.GFM,
|
|
extension.Footnote,
|
|
extension.DefinitionList,
|
|
extension.Table,
|
|
extension.Strikethrough,
|
|
extension.TaskList,
|
|
extension.Typographer,
|
|
),
|
|
goldmark.WithParserOptions(
|
|
parser.WithAutoHeadingID(),
|
|
),
|
|
goldmark.WithRendererOptions(
|
|
html.WithHardWraps(),
|
|
html.WithXHTML(),
|
|
),
|
|
),
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) Render(content []byte) (Result, error) {
|
|
title := extractTitle(string(content))
|
|
tags := extractTags(string(content))
|
|
prepared := preprocess(string(content))
|
|
|
|
var output bytes.Buffer
|
|
if err := r.markdown.Convert([]byte(prepared), &output); err != nil {
|
|
return Result{}, fmt.Errorf("render markdown: %w", err)
|
|
}
|
|
|
|
return Result{
|
|
HTML: template.HTML(output.String()),
|
|
Title: title,
|
|
Tags: tags,
|
|
}, nil
|
|
}
|
|
|
|
func preprocess(input string) string {
|
|
lines := strings.Split(input, "\n")
|
|
for index, line := range lines {
|
|
line = rewriteWikiLinks(line)
|
|
line = normalizeAdmonition(line)
|
|
lines[index] = line
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func rewriteWikiLinks(line string) string {
|
|
var builder strings.Builder
|
|
remaining := line
|
|
|
|
for {
|
|
start := strings.Index(remaining, "[[")
|
|
if start == -1 {
|
|
builder.WriteString(remaining)
|
|
return builder.String()
|
|
}
|
|
|
|
end := strings.Index(remaining[start+2:], "]]")
|
|
if end == -1 {
|
|
builder.WriteString(remaining)
|
|
return builder.String()
|
|
}
|
|
|
|
builder.WriteString(remaining[:start])
|
|
label := strings.TrimSpace(remaining[start+2 : start+2+end])
|
|
if label == "" {
|
|
builder.WriteString(remaining[start : start+2+end+2])
|
|
} else {
|
|
builder.WriteString(fmt.Sprintf("[%s](/docs/%s)", label, slugify(label)))
|
|
}
|
|
|
|
remaining = remaining[start+2+end+2:]
|
|
}
|
|
}
|
|
|
|
func normalizeAdmonition(line string) string {
|
|
switch {
|
|
case strings.HasPrefix(line, "> [!NOTE]"):
|
|
return strings.Replace(line, "> [!NOTE]", "> **Note:**", 1)
|
|
case strings.HasPrefix(line, "> [!TIP]"):
|
|
return strings.Replace(line, "> [!TIP]", "> **Tip:**", 1)
|
|
case strings.HasPrefix(line, "> [!WARNING]"):
|
|
return strings.Replace(line, "> [!WARNING]", "> **Warning:**", 1)
|
|
default:
|
|
return line
|
|
}
|
|
}
|
|
|
|
func extractTitle(content string) string {
|
|
for _, line := range strings.Split(content, "\n") {
|
|
if strings.HasPrefix(line, "# ") {
|
|
return strings.TrimSpace(strings.TrimPrefix(line, "# "))
|
|
}
|
|
}
|
|
return "Untitled"
|
|
}
|
|
|
|
func extractTags(content string) []string {
|
|
matches := tagPattern.FindAllStringSubmatch(content, -1)
|
|
tags := make([]string, 0, len(matches))
|
|
seen := make(map[string]struct{}, len(matches))
|
|
for _, match := range matches {
|
|
if len(match) < 3 {
|
|
continue
|
|
}
|
|
tag := match[2]
|
|
if _, exists := seen[tag]; exists {
|
|
continue
|
|
}
|
|
seen[tag] = struct{}{}
|
|
tags = append(tags, tag)
|
|
}
|
|
slices.Sort(tags)
|
|
return tags
|
|
}
|
|
|
|
func slugify(value string) string {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
var builder strings.Builder
|
|
lastDash := false
|
|
for _, r := range value {
|
|
switch {
|
|
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
|
|
builder.WriteRune(r)
|
|
lastDash = false
|
|
default:
|
|
if !lastDash {
|
|
builder.WriteByte('-')
|
|
lastDash = true
|
|
}
|
|
}
|
|
}
|
|
return strings.Trim(builder.String(), "-")
|
|
}
|