1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package main

import (
	"os/exec"
	"strings"
	"testing"
)

// Heading anchors (headingAnchorTransformer): every heading gets a slug id and
// becomes a link to itself, except when it already contains a link.
func TestRenderMarkdownHeadingAnchors(t *testing.T) {
	for _, tc := range []struct {
		name string
		md   string
		want []string
		deny []string
	}{
		{
			name: "simple heading links to itself",
			md:   "# Hello World\n\ntext\n",
			want: []string{
				`<h1 id="hello-world">`,
				`<a href="#hello-world" class="permalink">Hello World</a>`,
			},
		},
		{
			name: "every level is anchored",
			md:   "# One\n\n## Two\n\n### Three\n",
			want: []string{
				`<h1 id="one"><a href="#one" class="permalink">One</a></h1>`,
				`<h2 id="two"><a href="#two" class="permalink">Two</a></h2>`,
				`<h3 id="three"><a href="#three" class="permalink">Three</a></h3>`,
			},
		},
		{
			name: "colliding headings get distinct ids",
			md:   "## Usage\n\na\n\n## Usage\n\nb\n",
			want: []string{`id="usage"`, `id="usage-1"`},
		},
		{
			name: "inline markup inside a heading is preserved",
			md:   "## The `foo` flag\n",
			want: []string{
				`<h2 id="the-foo-flag">`,
				`<a href="#the-foo-flag" class="permalink">The <code>foo</code> flag</a>`,
			},
		},
		{
			// Wrapping would nest <a> inside <a>, which is invalid HTML. Such
			// a heading still gets an id, though goldmark slugifies the raw
			// source line, so the destination leaks into it — stock goldmark
			// behaviour, and only ever for the rare heading holding a link.
			name: "heading that already contains a link is left alone",
			md:   "## See [the docs](https://example.com)\n",
			want: []string{`<h2 id="see-the-docshttpsexamplecom">`, `href="https://example.com"`},
			deny: []string{`permalink`},
		},
		{
			name: "heading that is entirely a link is left alone",
			md:   "## [Docs](https://example.com)\n",
			deny: []string{`permalink`},
		},
		{
			name: "autolink in a heading is left alone",
			md:   "## <https://example.com>\n",
			deny: []string{`permalink`},
		},
		{
			// The anchor must not disturb the relative-URL rewriting that
			// relURLTransformer does for ordinary links.
			name: "relative links still resolve against the file's directory",
			md:   "# Title\n\n[x](sub/file.md)\n",
			want: []string{`href="/proj/dir/sub/file.md"`, `class="permalink"`},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			got, err := renderMarkdown([]byte(tc.md), "proj", "dir/README.md")
			if err != nil {
				t.Fatalf("renderMarkdown: %v", err)
			}
			for _, w := range tc.want {
				if !strings.Contains(got, w) {
					t.Errorf("missing %q in:\n%s", w, got)
				}
			}
			for _, d := range tc.deny {
				if strings.Contains(got, d) {
					t.Errorf("unexpected %q in:\n%s", d, got)
				}
			}
		})
	}
}

// The same anchoring must apply to manpages, which reach goldmark via mandoc's
// markdown backend rather than from a .md file.
func TestRenderManpageHeadingAnchors(t *testing.T) {
	if _, err := exec.LookPath("mandoc"); err != nil {
		t.Skip("mandoc not on PATH")
	}
	man := `.Dd January 1, 2026
.Dt TEST 1
.Os
.Sh NAME
.Nm test
.Nd a test page
.Sh IMPLEMENTATION NOTES
Some prose.
.Ss A Subsection
More prose.
`
	got, err := renderManpage([]byte(man), "proj", "test.1")
	if err != nil {
		t.Fatalf("renderManpage: %v", err)
	}
	for _, w := range []string{
		`<h1 id="name"><a href="#name" class="permalink">NAME</a></h1>`,
		`<h1 id="implementation-notes"><a href="#implementation-notes" class="permalink">IMPLEMENTATION NOTES</a></h1>`,
		`<h2 id="a-subsection"><a href="#a-subsection" class="permalink">A Subsection</a></h2>`,
	} {
		if !strings.Contains(got, w) {
			t.Errorf("missing %q in:\n%s", w, got)
		}
	}
}