Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,38 @@ This file summarizes notable repository changes in a compact, release-oriented f

### <a id='unreleased-git-log'></a>Unreleased Git Log

Used git range: `v1.0.1..HEAD`

```txt
```

## <a id='v1.0.1-changes'></a>v1.0.1 Changes (2026-05-30)

### <a id='v1.0.1-overview'></a>v1.0.1 Overview

* Generated ToC links now reuse explicit managed anchor IDs when `anchor=true`:
* `numbering=true` still affects ToC labels and heading display text
* link targets stay tied to the generated unnumbered `<a id="..."></a>` values
* duplicate headings keep matching ToC and anchor suffixes such as `summary` and `summary-1`
* VS Code Marketplace publishing is more robust:
* VSIX publishing now retries transient `vsce publish` failures
* already-published package responses are treated as non-fatal after the VSIX assets have been validated
* Extension development dependency lockfile updates:
* `qs` was updated from `6.15.1` to `6.15.2`
* `tmp` was updated from `0.2.5` to `0.2.7`

### <a id='v1.0.1-git-log'></a>v1.0.1 Git Log

Used git range: `v1.0.0..HEAD`

```txt
* 838007c Issue #109 fixed
* 7a37122 Merge pull request #108 from rokath/dependabot/npm_and_yarn/extension/tmp-0.2.7
* afcaafb Merge pull request #107 from rokath/dependabot/npm_and_yarn/extension/qs-6.15.2
* 57f2cc9 build(deps-dev): bump tmp from 0.2.5 to 0.2.7 in /extension
* c4aae24 build(deps-dev): bump qs from 6.15.1 to 6.15.2 in /extension
* e173126 Merge pull request #106 from rokath/dev
* b9c11fa retry logic added for poblishing
```

## <a id='v1.0.0-changes'></a>v1.0.0 Changes (2026-05-20)
Expand Down
4 changes: 2 additions & 2 deletions extension/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "mdtoc",
"displayName": "mdtoc - Markdown ToC with section numbering and stable links",
"description": "Generate, strip and configure a Markdown table of contents. Toggle ToC, numbering and links; choose heading levels, bullet style and slug method.",
"version": "1.0.0",
"version": "1.0.1",
"publisher": "rokath",
"license": "MIT",
"icon": "mdtoc_icon_128.png",
Expand Down
10 changes: 7 additions & 3 deletions internal/mdtoc/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,13 @@ func assignDerivedArtifacts(headings []Heading, cfg Config) {
h.ManagedNumber = ""
}

// Explicit managed anchors own the document target identity. Reusing
// anchorID keeps ToC hrefs tied to the exact ID rendered into headings
// instead of independently deriving a renderer-style slug.
if cfg.Anchor {
h.ManagedTOCTarget = anchorID
continue
}
h.ManagedTOCTarget = tocSlugger.Next(tocSlugSource(*h, cfg))
}
}
Expand All @@ -243,9 +250,6 @@ func anchorSlugSource(h Heading, cfg Config) string {
}

func tocSlugSource(h Heading, cfg Config) string {
if cfg.Anchor {
return anchorSlugSource(h, cfg)
}
source := h.TitleText
switch cfg.Slug {
case SlugCrossnote:
Expand Down
137 changes: 137 additions & 0 deletions internal/mdtoc/process_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,143 @@ func TestGenerateLinkFalseRendersPlainTOC(t *testing.T) {
}
}

// TestGenerateTOCTargetsFollowAnchorOwnershipMatrix verifies the issue #109
// invariant across the persisted config flags that decide whether mdtoc owns
// heading IDs or delegates them to the selected renderer slug profile.
func TestGenerateTOCTargetsFollowAnchorOwnershipMatrix(t *testing.T) {
input := "# Title\n\n## Foo Bar\n## Foo Bar\n"
slugCases := []struct {
slug SlugMode
anchorTargets [2]string
plainTargets [2]string
numberedTargets [2]string
}{
{
slug: SlugGitHub,
anchorTargets: [2]string{"foo--bar", "foo--bar-1"},
plainTargets: [2]string{"foo--bar", "foo--bar-1"},
numberedTargets: [2]string{"1-foo--bar", "2-foo--bar"},
},
{
slug: SlugGitLab,
anchorTargets: [2]string{"foo-bar", "foo-bar-1"},
plainTargets: [2]string{"foo-bar", "foo-bar-1"},
numberedTargets: [2]string{"1-foo-bar", "2-foo-bar"},
},
{
slug: SlugCrossnote,
anchorTargets: [2]string{"foo--bar", "foo--bar-1"},
plainTargets: [2]string{"foo--bar", "foo--bar-1"},
numberedTargets: [2]string{"1-foo--bar", "2-foo--bar"},
},
}

for _, sc := range slugCases {
for _, numbering := range []bool{false, true} {
for _, anchor := range []bool{false, true} {
for _, link := range []bool{false, true} {
for _, toc := range []bool{false, true} {
name := fmt.Sprintf("slug=%s/numbering=%t/anchor=%t/link=%t/toc=%t", sc.slug, numbering, anchor, link, toc)
t.Run(name, func(t *testing.T) {
got, _, err := Generate(input, Options{
Numbering: numbering,
MinLevel: 2,
MaxLevel: 2,
Slug: sc.slug,
Anchor: anchor,
AnchorSet: true,
Link: link,
LinkSet: true,
TOC: toc,
Bullets: BulletStar,
})
if err != nil {
t.Fatalf("Generate error: %v", err)
}

firstLabel := "Foo Bar"
secondLabel := "Foo Bar"
if numbering {
firstLabel = "1. " + firstLabel
secondLabel = "2. " + secondLabel
}

firstHeading := "## "
secondHeading := "## "
if numbering {
firstHeading += "1. "
secondHeading += "2. "
}
if anchor {
firstHeading += fmt.Sprintf(`<a id="%s"></a>`, sc.anchorTargets[0])
secondHeading += fmt.Sprintf(`<a id="%s"></a>`, sc.anchorTargets[1])
}
firstHeading += "Foo Bar"
secondHeading += "Foo Bar"

for _, want := range []string{firstHeading, secondHeading} {
if !strings.Contains(got, want) {
t.Fatalf("generated output missing heading %q:\n%s", want, got)
}
}
if !anchor && strings.Contains(got, "<a id=") {
t.Fatalf("anchor=false unexpectedly rendered inline anchors:\n%s", got)
}

targets := sc.plainTargets
if numbering {
targets = sc.numberedTargets
}
if anchor {
targets = sc.anchorTargets
}

if toc {
if link {
for _, want := range []string{
fmt.Sprintf("* [%s](#%s)", firstLabel, targets[0]),
fmt.Sprintf("* [%s](#%s)", secondLabel, targets[1]),
} {
if !strings.Contains(got, want) {
t.Fatalf("generated output missing linked ToC entry %q:\n%s", want, got)
}
}
if anchor && numbering {
for _, forbidden := range sc.numberedTargets {
if strings.Contains(got, fmt.Sprintf("](#%s)", forbidden)) {
t.Fatalf("anchor=true ToC used numbered renderer target %q:\n%s", forbidden, got)
}
}
}
} else {
for _, want := range []string{
"* " + firstLabel,
"* " + secondLabel,
} {
if !strings.Contains(got, want) {
t.Fatalf("generated output missing plain ToC entry %q:\n%s", want, got)
}
}
if strings.Contains(got, "](#") {
t.Fatalf("link=false rendered linked ToC entries:\n%s", got)
}
}
} else if strings.Contains(got, "* [") || strings.Contains(got, "* Foo Bar") || strings.Contains(got, "* 1. Foo Bar") || strings.Contains(got, "* 2. Foo Bar") {
t.Fatalf("toc=false rendered ToC entries:\n%s", got)
}

ok, _, err := Check(got)
if err != nil || !ok {
t.Fatalf("Check(generated) = %v, %v\n%s", ok, err, got)
}
})
}
}
}
}
}
}

// TestGenerateAnchorOffNumberingUsesRenderedHeadingSlugForTOC verifies issue #75.
func TestGenerateAnchorOffNumberingUsesRenderedHeadingSlugForTOC(t *testing.T) {
input := "# Title\n\n## Intro\n\n### API\n"
Expand Down
Loading