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
Commit
8b062e96bcaaafd98cef1ee4d2cc0336584c8ec2f10802ab1a955ec41afbce02
Parent
c0276b7f148c74f…
1 file changed
+55
-5
+55
-5
| --- fossil/views.py | ||
| +++ fossil/views.py | ||
| @@ -11,11 +11,11 @@ | ||
| 11 | 11 | |
| 12 | 12 | from .models import FossilRepository |
| 13 | 13 | from .reader import FossilReader |
| 14 | 14 | |
| 15 | 15 | |
| 16 | -def _render_fossil_content(content: str) -> str: | |
| 16 | +def _render_fossil_content(content: str, project_slug: str = "") -> str: | |
| 17 | 17 | """Render content that may be Fossil wiki markup, HTML, or Markdown. |
| 18 | 18 | |
| 19 | 19 | Fossil wiki pages can contain: |
| 20 | 20 | - Raw HTML (most Fossil wiki pages) |
| 21 | 21 | - Fossil-specific markup: [link|text], <verbatim>...</verbatim> |
| @@ -29,11 +29,12 @@ | ||
| 29 | 29 | |
| 30 | 30 | if is_markdown: |
| 31 | 31 | # Markdown: convert Fossil [/path|text] links to markdown links first |
| 32 | 32 | content = re.sub(r"\[(/[^|\]]+)\|([^\]]+)\]", r"[\2](\1)", content) |
| 33 | 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"]) | |
| 34 | + html = md.markdown(content, extensions=["fenced_code", "tables", "toc"]) | |
| 35 | + return _rewrite_fossil_links(html, project_slug) if project_slug else html | |
| 35 | 36 | |
| 36 | 37 | # Fossil wiki / HTML: convert Fossil-specific syntax to HTML |
| 37 | 38 | content = re.sub(r"\[(/[^|\]]+)\|([^\]]+)\]", r'<a href="\1">\2</a>', content) |
| 38 | 39 | content = re.sub(r"\[(https?://[^|\]]+)\|([^\]]+)\]", r'<a href="\1">\2</a>', content) |
| 39 | 40 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"<pre><code>\1</code></pre>", content, flags=re.DOTALL) |
| @@ -61,11 +62,11 @@ | ||
| 61 | 62 | content = "\n".join(result) |
| 62 | 63 | |
| 63 | 64 | # Wrap bare text blocks in <p> tags (lines not inside HTML tags) |
| 64 | 65 | content = re.sub(r"\n\n(?!<)", "\n\n<p>", content) |
| 65 | 66 | |
| 66 | - return content | |
| 67 | + return _rewrite_fossil_links(content, project_slug) if project_slug else content | |
| 67 | 68 | |
| 68 | 69 | |
| 69 | 70 | def _is_markdown(content: str) -> bool: |
| 70 | 71 | """Detect if content is Markdown vs Fossil wiki/HTML. |
| 71 | 72 | |
| @@ -88,10 +89,59 @@ | ||
| 88 | 89 | # Starts with HTML block element — it's Fossil wiki/HTML |
| 89 | 90 | if re.match(r"<(h[1-6]|p|ol|ul|div|table)\b", stripped, re.IGNORECASE): |
| 90 | 91 | return False |
| 91 | 92 | return False |
| 92 | 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 | + | |
| 93 | 143 | |
| 94 | 144 | def _get_repo_and_reader(slug): |
| 95 | 145 | """Return (project, fossil_repo, reader) or raise 404.""" |
| 96 | 146 | project = get_object_or_404(Project, slug=slug, deleted_at__isnull=True) |
| 97 | 147 | fossil_repo = get_object_or_404(FossilRepository, project=project, deleted_at__isnull=True) |
| @@ -337,11 +387,11 @@ | ||
| 337 | 387 | pages = reader.get_wiki_pages() |
| 338 | 388 | home_page = reader.get_wiki_page("Home") |
| 339 | 389 | |
| 340 | 390 | home_content_html = "" |
| 341 | 391 | 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)) | |
| 343 | 393 | |
| 344 | 394 | return render( |
| 345 | 395 | request, |
| 346 | 396 | "fossil/wiki_list.html", |
| 347 | 397 | { |
| @@ -365,11 +415,11 @@ | ||
| 365 | 415 | all_pages = reader.get_wiki_pages() |
| 366 | 416 | |
| 367 | 417 | if not page: |
| 368 | 418 | raise Http404(f"Wiki page not found: {page_name}") |
| 369 | 419 | |
| 370 | - content_html = mark_safe(_render_fossil_content(page.content)) | |
| 420 | + content_html = mark_safe(_render_fossil_content(page.content, project_slug=slug)) | |
| 371 | 421 | |
| 372 | 422 | return render( |
| 373 | 423 | request, |
| 374 | 424 | "fossil/wiki_page.html", |
| 375 | 425 | { |
| 376 | 426 |
| --- 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 |