FossilRepo

Rewrite internal Fossil links to app URLs in wiki content - /info/HASH -> /fossil/checkin/HASH/ - /doc/trunk/www/file -> /fossil/code/file/www/file - /wiki/PageName and /wiki?name=PageName -> /fossil/wiki/page/PageName - /tktview/HASH -> /fossil/tickets/HASH/ - /timeline, /forum -> respective app pages - External links (https://) preserved as-is - Fixes broken links when clicking through wiki pages

lmata 2026-04-06 14:04 trunk
Commit 8b062e96bcaaafd98cef1ee4d2cc0336584c8ec2f10802ab1a955ec41afbce02
1 file changed +55 -5
+55 -5
--- fossil/views.py
+++ fossil/views.py
@@ -11,11 +11,11 @@
1111
1212
from .models import FossilRepository
1313
from .reader import FossilReader
1414
1515
16
-def _render_fossil_content(content: str) -> str:
16
+def _render_fossil_content(content: str, project_slug: str = "") -> str:
1717
"""Render content that may be Fossil wiki markup, HTML, or Markdown.
1818
1919
Fossil wiki pages can contain:
2020
- Raw HTML (most Fossil wiki pages)
2121
- Fossil-specific markup: [link|text], <verbatim>...</verbatim>
@@ -29,11 +29,12 @@
2929
3030
if is_markdown:
3131
# Markdown: convert Fossil [/path|text] links to markdown links first
3232
content = re.sub(r"\[(/[^|\]]+)\|([^\]]+)\]", r"[\2](\1)", content)
3333
content = re.sub(r"<verbatim>(.*?)</verbatim>", r"```\n\1\n```", content, flags=re.DOTALL)
34
- return md.markdown(content, extensions=["fenced_code", "tables", "toc"])
34
+ html = md.markdown(content, extensions=["fenced_code", "tables", "toc"])
35
+ return _rewrite_fossil_links(html, project_slug) if project_slug else html
3536
3637
# Fossil wiki / HTML: convert Fossil-specific syntax to HTML
3738
content = re.sub(r"\[(/[^|\]]+)\|([^\]]+)\]", r'<a href="\1">\2</a>', content)
3839
content = re.sub(r"\[(https?://[^|\]]+)\|([^\]]+)\]", r'<a href="\1">\2</a>', content)
3940
content = re.sub(r"<verbatim>(.*?)</verbatim>", r"<pre><code>\1</code></pre>", content, flags=re.DOTALL)
@@ -61,11 +62,11 @@
6162
content = "\n".join(result)
6263
6364
# Wrap bare text blocks in <p> tags (lines not inside HTML tags)
6465
content = re.sub(r"\n\n(?!<)", "\n\n<p>", content)
6566
66
- return content
67
+ return _rewrite_fossil_links(content, project_slug) if project_slug else content
6768
6869
6970
def _is_markdown(content: str) -> bool:
7071
"""Detect if content is Markdown vs Fossil wiki/HTML.
7172
@@ -88,10 +89,59 @@
8889
# Starts with HTML block element — it's Fossil wiki/HTML
8990
if re.match(r"<(h[1-6]|p|ol|ul|div|table)\b", stripped, re.IGNORECASE):
9091
return False
9192
return False
9293
94
+
95
+def _rewrite_fossil_links(html: str, project_slug: str) -> str:
96
+ """Rewrite internal Fossil URLs to our app's URL structure.
97
+
98
+ Fossil links like /doc/trunk/www/file.wiki, /info/HASH, /wiki/PageName,
99
+ /tktview/HASH get mapped to our fossil app URLs.
100
+ """
101
+ if not project_slug:
102
+ return html
103
+
104
+ base = f"/projects/{project_slug}/fossil"
105
+
106
+ def replace_link(match):
107
+ url = match.group(1)
108
+ # /info/HASH -> checkin detail
109
+ m = re.match(r"/info/([0-9a-f]+)", url)
110
+ if m:
111
+ return f'href="{base}/checkin/{m.group(1)}/"'
112
+ # /doc/trunk/www/file or /doc/tip/... -> code file view
113
+ m = re.match(r"/doc/(?:trunk|tip|[^/]+)/(.+)", url)
114
+ if m:
115
+ return f'href="{base}/code/file/{m.group(1)}"'
116
+ # /wiki?name=PageName -> wiki page (query string format)
117
+ m = re.match(r"/wiki\?name=(.+)", url)
118
+ if m:
119
+ return f'href="{base}/wiki/page/{m.group(1)}"'
120
+ # /wiki/PageName -> wiki page (path format)
121
+ m = re.match(r"/wiki/(.+)", url)
122
+ if m:
123
+ return f'href="{base}/wiki/page/{m.group(1)}"'
124
+ # /tktview/HASH or /tktview?name=HASH -> ticket detail
125
+ m = re.match(r"/tktview[?/](?:name=)?([0-9a-f]+)", url)
126
+ if m:
127
+ return f'href="{base}/tickets/{m.group(1)}/"'
128
+ # /timeline -> timeline
129
+ if url.startswith("/timeline"):
130
+ return f'href="{base}/timeline/"'
131
+ # /forum -> forum
132
+ if url.startswith("/forumpost") or url.startswith("/forum"):
133
+ return f'href="{base}/forum/"'
134
+ # Keep external and unrecognized links as-is
135
+ return match.group(0)
136
+
137
+ # Rewrite href="/..." links (internal Fossil paths)
138
+ html = re.sub(r'href="(/[^"]*)"', replace_link, html)
139
+ # Also rewrite href="/wiki?name=..." (markdown renders these with full path)
140
+ html = re.sub(r'href="(/wiki\?[^"]*)"', replace_link, html)
141
+ return html
142
+
93143
94144
def _get_repo_and_reader(slug):
95145
"""Return (project, fossil_repo, reader) or raise 404."""
96146
project = get_object_or_404(Project, slug=slug, deleted_at__isnull=True)
97147
fossil_repo = get_object_or_404(FossilRepository, project=project, deleted_at__isnull=True)
@@ -337,11 +387,11 @@
337387
pages = reader.get_wiki_pages()
338388
home_page = reader.get_wiki_page("Home")
339389
340390
home_content_html = ""
341391
if home_page:
342
- home_content_html = mark_safe(_render_fossil_content(home_page.content))
392
+ home_content_html = mark_safe(_render_fossil_content(home_page.content, project_slug=slug))
343393
344394
return render(
345395
request,
346396
"fossil/wiki_list.html",
347397
{
@@ -365,11 +415,11 @@
365415
all_pages = reader.get_wiki_pages()
366416
367417
if not page:
368418
raise Http404(f"Wiki page not found: {page_name}")
369419
370
- content_html = mark_safe(_render_fossil_content(page.content))
420
+ content_html = mark_safe(_render_fossil_content(page.content, project_slug=slug))
371421
372422
return render(
373423
request,
374424
"fossil/wiki_page.html",
375425
{
376426
--- fossil/views.py
+++ fossil/views.py
@@ -11,11 +11,11 @@
11
12 from .models import FossilRepository
13 from .reader import FossilReader
14
15
16 def _render_fossil_content(content: str) -> str:
17 """Render content that may be Fossil wiki markup, HTML, or Markdown.
18
19 Fossil wiki pages can contain:
20 - Raw HTML (most Fossil wiki pages)
21 - Fossil-specific markup: [link|text], <verbatim>...</verbatim>
@@ -29,11 +29,12 @@
29
30 if is_markdown:
31 # Markdown: convert Fossil [/path|text] links to markdown links first
32 content = re.sub(r"\[(/[^|\]]+)\|([^\]]+)\]", r"[\2](\1)", content)
33 content = re.sub(r"<verbatim>(.*?)</verbatim>", r"```\n\1\n```", content, flags=re.DOTALL)
34 return md.markdown(content, extensions=["fenced_code", "tables", "toc"])
 
35
36 # Fossil wiki / HTML: convert Fossil-specific syntax to HTML
37 content = re.sub(r"\[(/[^|\]]+)\|([^\]]+)\]", r'<a href="\1">\2</a>', content)
38 content = re.sub(r"\[(https?://[^|\]]+)\|([^\]]+)\]", r'<a href="\1">\2</a>', content)
39 content = re.sub(r"<verbatim>(.*?)</verbatim>", r"<pre><code>\1</code></pre>", content, flags=re.DOTALL)
@@ -61,11 +62,11 @@
61 content = "\n".join(result)
62
63 # Wrap bare text blocks in <p> tags (lines not inside HTML tags)
64 content = re.sub(r"\n\n(?!<)", "\n\n<p>", content)
65
66 return content
67
68
69 def _is_markdown(content: str) -> bool:
70 """Detect if content is Markdown vs Fossil wiki/HTML.
71
@@ -88,10 +89,59 @@
88 # Starts with HTML block element — it's Fossil wiki/HTML
89 if re.match(r"<(h[1-6]|p|ol|ul|div|table)\b", stripped, re.IGNORECASE):
90 return False
91 return False
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
94 def _get_repo_and_reader(slug):
95 """Return (project, fossil_repo, reader) or raise 404."""
96 project = get_object_or_404(Project, slug=slug, deleted_at__isnull=True)
97 fossil_repo = get_object_or_404(FossilRepository, project=project, deleted_at__isnull=True)
@@ -337,11 +387,11 @@
337 pages = reader.get_wiki_pages()
338 home_page = reader.get_wiki_page("Home")
339
340 home_content_html = ""
341 if home_page:
342 home_content_html = mark_safe(_render_fossil_content(home_page.content))
343
344 return render(
345 request,
346 "fossil/wiki_list.html",
347 {
@@ -365,11 +415,11 @@
365 all_pages = reader.get_wiki_pages()
366
367 if not page:
368 raise Http404(f"Wiki page not found: {page_name}")
369
370 content_html = mark_safe(_render_fossil_content(page.content))
371
372 return render(
373 request,
374 "fossil/wiki_page.html",
375 {
376
--- fossil/views.py
+++ fossil/views.py
@@ -11,11 +11,11 @@
11
12 from .models import FossilRepository
13 from .reader import FossilReader
14
15
16 def _render_fossil_content(content: str, project_slug: str = "") -> str:
17 """Render content that may be Fossil wiki markup, HTML, or Markdown.
18
19 Fossil wiki pages can contain:
20 - Raw HTML (most Fossil wiki pages)
21 - Fossil-specific markup: [link|text], <verbatim>...</verbatim>
@@ -29,11 +29,12 @@
29
30 if is_markdown:
31 # Markdown: convert Fossil [/path|text] links to markdown links first
32 content = re.sub(r"\[(/[^|\]]+)\|([^\]]+)\]", r"[\2](\1)", content)
33 content = re.sub(r"<verbatim>(.*?)</verbatim>", r"```\n\1\n```", content, flags=re.DOTALL)
34 html = md.markdown(content, extensions=["fenced_code", "tables", "toc"])
35 return _rewrite_fossil_links(html, project_slug) if project_slug else html
36
37 # Fossil wiki / HTML: convert Fossil-specific syntax to HTML
38 content = re.sub(r"\[(/[^|\]]+)\|([^\]]+)\]", r'<a href="\1">\2</a>', content)
39 content = re.sub(r"\[(https?://[^|\]]+)\|([^\]]+)\]", r'<a href="\1">\2</a>', content)
40 content = re.sub(r"<verbatim>(.*?)</verbatim>", r"<pre><code>\1</code></pre>", content, flags=re.DOTALL)
@@ -61,11 +62,11 @@
62 content = "\n".join(result)
63
64 # Wrap bare text blocks in <p> tags (lines not inside HTML tags)
65 content = re.sub(r"\n\n(?!<)", "\n\n<p>", content)
66
67 return _rewrite_fossil_links(content, project_slug) if project_slug else content
68
69
70 def _is_markdown(content: str) -> bool:
71 """Detect if content is Markdown vs Fossil wiki/HTML.
72
@@ -88,10 +89,59 @@
89 # Starts with HTML block element — it's Fossil wiki/HTML
90 if re.match(r"<(h[1-6]|p|ol|ul|div|table)\b", stripped, re.IGNORECASE):
91 return False
92 return False
93
94
95 def _rewrite_fossil_links(html: str, project_slug: str) -> str:
96 """Rewrite internal Fossil URLs to our app's URL structure.
97
98 Fossil links like /doc/trunk/www/file.wiki, /info/HASH, /wiki/PageName,
99 /tktview/HASH get mapped to our fossil app URLs.
100 """
101 if not project_slug:
102 return html
103
104 base = f"/projects/{project_slug}/fossil"
105
106 def replace_link(match):
107 url = match.group(1)
108 # /info/HASH -> checkin detail
109 m = re.match(r"/info/([0-9a-f]+)", url)
110 if m:
111 return f'href="{base}/checkin/{m.group(1)}/"'
112 # /doc/trunk/www/file or /doc/tip/... -> code file view
113 m = re.match(r"/doc/(?:trunk|tip|[^/]+)/(.+)", url)
114 if m:
115 return f'href="{base}/code/file/{m.group(1)}"'
116 # /wiki?name=PageName -> wiki page (query string format)
117 m = re.match(r"/wiki\?name=(.+)", url)
118 if m:
119 return f'href="{base}/wiki/page/{m.group(1)}"'
120 # /wiki/PageName -> wiki page (path format)
121 m = re.match(r"/wiki/(.+)", url)
122 if m:
123 return f'href="{base}/wiki/page/{m.group(1)}"'
124 # /tktview/HASH or /tktview?name=HASH -> ticket detail
125 m = re.match(r"/tktview[?/](?:name=)?([0-9a-f]+)", url)
126 if m:
127 return f'href="{base}/tickets/{m.group(1)}/"'
128 # /timeline -> timeline
129 if url.startswith("/timeline"):
130 return f'href="{base}/timeline/"'
131 # /forum -> forum
132 if url.startswith("/forumpost") or url.startswith("/forum"):
133 return f'href="{base}/forum/"'
134 # Keep external and unrecognized links as-is
135 return match.group(0)
136
137 # Rewrite href="/..." links (internal Fossil paths)
138 html = re.sub(r'href="(/[^"]*)"', replace_link, html)
139 # Also rewrite href="/wiki?name=..." (markdown renders these with full path)
140 html = re.sub(r'href="(/wiki\?[^"]*)"', replace_link, html)
141 return html
142
143
144 def _get_repo_and_reader(slug):
145 """Return (project, fossil_repo, reader) or raise 404."""
146 project = get_object_or_404(Project, slug=slug, deleted_at__isnull=True)
147 fossil_repo = get_object_or_404(FossilRepository, project=project, deleted_at__isnull=True)
@@ -337,11 +387,11 @@
387 pages = reader.get_wiki_pages()
388 home_page = reader.get_wiki_page("Home")
389
390 home_content_html = ""
391 if home_page:
392 home_content_html = mark_safe(_render_fossil_content(home_page.content, project_slug=slug))
393
394 return render(
395 request,
396 "fossil/wiki_list.html",
397 {
@@ -365,11 +415,11 @@
415 all_pages = reader.get_wiki_pages()
416
417 if not page:
418 raise Http404(f"Wiki page not found: {page_name}")
419
420 content_html = mark_safe(_render_fossil_content(page.content, project_slug=slug))
421
422 return render(
423 request,
424 "fossil/wiki_page.html",
425 {
426

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button