internal/renderer/links.go

package renderer

import (
	"path/filepath"
	"strings"

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

type linkTransformer struct{}

func (t *linkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
		if !entering {
			return ast.WalkContinue, nil
		}

		link, ok := n.(*ast.Link)
		if !ok {
			return ast.WalkContinue, nil
		}

		dest := string(link.Destination)
		if isRelativeMarkdown(dest) {
			newDest := modifyLink(dest)
			link.Destination = []byte(newDest)
		}

		return ast.WalkContinue, nil
	})
}

func isRelativeMarkdown(dest string) bool {
	// Skip absolute URLs
	if strings.Contains(dest, "://") || strings.HasPrefix(dest, "/") || strings.HasPrefix(dest, "mailto:") || strings.HasPrefix(dest, "tel:") {
		return false
	}
	// Skip anchors
	if strings.HasPrefix(dest, "#") {
		return false
	}

	// It's relative. Check if it's a markdown file or extensionless
	ext := filepath.Ext(dest)
	if ext == "" || strings.ToLower(ext) == ".md" {
		return true
	}

	return false
}

func modifyLink(dest string) string {
	destLower := strings.ToLower(dest)

	// Handle readme.md -> index.html
	if strings.HasSuffix(destLower, "readme.md") {
		// Ensure we handle "readme.md" and "path/to/readme.md"
		if destLower == "readme.md" {
			return "index.html"
		}
		if strings.HasSuffix(destLower, "/readme.md") {
			return dest[:len(dest)-9] + "index.html"
		}
	}

	// Handle other .md files -> .md.html
	if strings.HasSuffix(destLower, ".md") {
		return dest + ".html"
	}

	// Handle extensionless links -> .html
	if filepath.Ext(dest) == "" {
		return dest + ".html"
	}

	return dest
}

type linkExtension struct{}

func (e *linkExtension) Extend(m goldmark.Markdown) {
	m.Parser().AddOptions(
		parser.WithASTTransformers(
			util.Prioritized(&linkTransformer{}, 100),
		),
	)
}

func NewLinkExtension() goldmark.Extender {
	return &linkExtension{}
}