Skip to content

Commit 1a2fed8

Browse files
authored
fix: fix links to fragments (#84)
1 parent 7b29ffd commit 1a2fed8

4 files changed

Lines changed: 21 additions & 15 deletions

File tree

src/rockgarden/content/store.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import TYPE_CHECKING
88

99
from rockgarden.content.models import Page
10-
from rockgarden.urls import get_url
10+
from rockgarden.urls import get_url, slugify_heading
1111

1212
if TYPE_CHECKING:
1313
from rockgarden.content.collection import Collection
@@ -121,7 +121,7 @@ def resolve_link(self, link_target: str) -> str | None:
121121
if page:
122122
url = get_url(page.slug, self.clean_urls, self.base_path)
123123
if fragment:
124-
url = f"{url}#{fragment}"
124+
url = f"{url}#{slugify_heading(fragment)}"
125125
return url
126126

127127
# Try to resolve as a media file

src/rockgarden/nav/toc.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from dataclasses import dataclass, field
55
from html import unescape
66

7+
from rockgarden.urls import slugify_heading
8+
79
HEADING_RE = re.compile(r"<(h[2-6])(\s[^>]*)?>(.+?)</\1>", re.DOTALL)
810
TAG_RE = re.compile(r"<[^>]+>")
911

@@ -18,14 +20,6 @@ class TocEntry:
1820
children: list["TocEntry"] = field(default_factory=list)
1921

2022

21-
def _slugify(text: str) -> str:
22-
"""Convert heading text to a URL-friendly slug."""
23-
text = text.lower()
24-
text = re.sub(r"[^\w\s-]", "", text)
25-
text = re.sub(r"[\s]+", "-", text.strip())
26-
return text
27-
28-
2923
def _strip_tags(html: str) -> str:
3024
"""Remove HTML tags and unescape entities to get plain text."""
3125
return unescape(TAG_RE.sub("", html).strip())
@@ -78,7 +72,7 @@ def _replace_heading(match: re.Match) -> str:
7872
if level < min_level or level > max_level:
7973
# Still inject ID for anchor linking, but don't add to TOC
8074
text = _strip_tags(inner_html)
81-
slug = _slugify(text)
75+
slug = slugify_heading(text)
8276
if not slug:
8377
return match.group(0)
8478
if slug in seen_ids:
@@ -91,7 +85,7 @@ def _replace_heading(match: re.Match) -> str:
9185
return f'<{tag}{attrs} id="{slug}">{inner_html}</{tag}>'
9286

9387
text = _strip_tags(inner_html)
94-
slug = _slugify(text)
88+
slug = slugify_heading(text)
9589
if not slug:
9690
return match.group(0)
9791

src/rockgarden/urls.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44
from urllib.parse import quote, urlparse
55

66

7+
def slugify_heading(text: str) -> str:
8+
"""Convert heading text to a URL-friendly slug.
9+
10+
Matches the slugification used for heading anchor IDs so that
11+
fragment links like ``[[Page#My Heading]]`` resolve correctly.
12+
"""
13+
text = text.lower()
14+
text = re.sub(r"[^\w\s-]", "", text)
15+
text = re.sub(r"[\s]+", "-", text.strip())
16+
return text
17+
18+
719
def normalize_tag(tag: str) -> str:
820
"""Normalize a tag to a URL-safe slug.
921

tests/test_content_store.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def test_section_link(self):
4646
store = ContentStore([page])
4747

4848
url = store.resolve_link("Chamber of the Stone#Thalador")
49-
assert url == "/chamber-of-the-stone/#Thalador"
49+
assert url == "/chamber-of-the-stone/#thalador"
5050

5151
def test_section_link_with_spaces(self):
5252
"""Should preserve spaces in section fragment."""
@@ -59,7 +59,7 @@ def test_section_link_with_spaces(self):
5959
store = ContentStore([page])
6060

6161
url = store.resolve_link("Session Log#Group Vision")
62-
assert url == "/session-log/#Group Vision"
62+
assert url == "/session-log/#group-vision"
6363

6464
def test_section_link_whitespace_trimmed(self):
6565
"""Should trim whitespace around page name and fragment."""
@@ -72,7 +72,7 @@ def test_section_link_whitespace_trimmed(self):
7272
store = ContentStore([page])
7373

7474
url = store.resolve_link("Notes # Section ")
75-
assert url == "/notes/#Section"
75+
assert url == "/notes/#section"
7676

7777
def test_broken_link_returns_none(self):
7878
"""Should return None for non-existent page."""

0 commit comments

Comments
 (0)