vitrine

slug.go

package main

import (
	"strconv"
	"strings"

	"github.com/yuin/goldmark/ast"
	"github.com/yuin/goldmark/parser"
	"github.com/yuin/goldmark/text"
)

// slugify turns heading text into a URL fragment:
// lowercase, alphanumerics kept, every other run of characters becomes one dash.
func slugify(s string) string {
	var b strings.Builder
	dash := false
	for _, r := range strings.ToLower(s) {
		switch {
		case r >= 'a' && r <= 'z' || r >= '0' && r <= '9':
			if dash && b.Len() > 0 {
				b.WriteByte('-')
			}
			dash = false
			b.WriteRune(r)
		default:
			dash = true
		}
	}
	return b.String()
}

// slugIDs implements parser.IDs so goldmark assigns our slugs as heading ids.
// Duplicate headings get "-2", "-3", ... suffixes.
type slugIDs struct {
	used map[string]bool
}

func newSlugIDs() *slugIDs {
	return &slugIDs{used: make(map[string]bool)}
}

func (s *slugIDs) Generate(value []byte, kind ast.NodeKind) []byte {
	slug := slugify(string(value))
	if slug == "" {
		slug = "section"
	}
	candidate := slug
	for n := 2; s.used[candidate]; n++ {
		candidate = slug + "-" + strconv.Itoa(n)
	}
	s.used[candidate] = true
	return []byte(candidate)
}

func (s *slugIDs) Put(value []byte) {
	s.used[string(value)] = true
}

// headingAnchors is a goldmark AST transformer that appends a chainlink
// anchor to every heading that received an id, linking to its own fragment.
// Pure HTML output: works without javascript.
type headingAnchors struct{}

func (headingAnchors) Transform(doc *ast.Document, reader text.Reader, pc parser.Context) {
	_ = ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
		if !entering {
			return ast.WalkContinue, nil
		}
		h, ok := n.(*ast.Heading)
		if !ok {
			return ast.WalkContinue, nil
		}
		id, ok := h.AttributeString("id")
		if !ok {
			return ast.WalkContinue, nil
		}
		link := ast.NewLink()
		link.Destination = append([]byte("#"), id.([]byte)...)
		link.SetAttributeString("class", []byte("anchor"))
		link.AppendChild(link, ast.NewString([]byte("\U0001F517"))) // chainlink emoji
		h.AppendChild(h, link)
		return ast.WalkContinue, nil
	})
}

slug.go · 2.1 KiB · 496af3b480