FossilRepo
Fix relative links in Fossil docs: resolve against base_path - _render_fossil_content now accepts base_path for relative link resolution - ./glossary.md in www/concepts.wiki → /www/glossary.md (not /glossary.md) - Bare filenames (quickstart.wiki) also resolved against base_path - _rewrite_fossil_links handles /www/*.wiki → /fossil/docs/www/... - /help/command links route to docs - Bare .wiki/.md/.html paths route to /fossil/docs/www/
Commit
23e8ddeccf99ae4f3a409517f8d616b390727bb79ef87560638a1aed784fb9b1
Parent
9313e3219cb2721…
1 file changed
+27
-5
+27
-5
| --- fossil/views.py | ||
| +++ fossil/views.py | ||
| @@ -11,17 +11,19 @@ | ||
| 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, project_slug: str = "") -> str: | |
| 16 | +def _render_fossil_content(content: str, project_slug: str = "", base_path: 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> |
| 22 | 22 | - Markdown (newer pages) |
| 23 | + | |
| 24 | + base_path: directory of the current file (e.g. "www/") for resolving relative links. | |
| 23 | 25 | """ |
| 24 | 26 | if not content: |
| 25 | 27 | return "" |
| 26 | 28 | |
| 27 | 29 | # Detect format from the raw content BEFORE any transformations |
| @@ -31,11 +33,13 @@ | ||
| 31 | 33 | # Markdown: convert Fossil [path | text] links to markdown links first |
| 32 | 34 | def _fossil_to_md_link(m): |
| 33 | 35 | path = m.group(1).strip() |
| 34 | 36 | text = m.group(2).strip() |
| 35 | 37 | if path.startswith("./"): |
| 36 | - path = "/" + path[2:] | |
| 38 | + path = "/" + base_path + path[2:] | |
| 39 | + elif not path.startswith("/") and not path.startswith("http"): | |
| 40 | + path = "/" + base_path + path | |
| 37 | 41 | return f"[{text}]({path})" |
| 38 | 42 | |
| 39 | 43 | content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_to_md_link, content) |
| 40 | 44 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"```\n\1\n```", content, flags=re.DOTALL) |
| 41 | 45 | html = md.markdown(content, extensions=["fenced_code", "tables", "toc"]) |
| @@ -44,13 +48,15 @@ | ||
| 44 | 48 | # Fossil wiki / HTML: convert Fossil-specific syntax to HTML |
| 45 | 49 | # Fossil links: [path | text] or [path|text] — spaces around pipe are optional |
| 46 | 50 | def _fossil_link_replace(match): |
| 47 | 51 | path = match.group(1).strip() |
| 48 | 52 | text = match.group(2).strip() |
| 49 | - # Convert relative paths (./foo) to absolute (/foo) | |
| 53 | + # Convert relative paths to absolute using base_path | |
| 50 | 54 | if path.startswith("./"): |
| 51 | - path = "/" + path[2:] | |
| 55 | + path = "/" + base_path + path[2:] | |
| 56 | + elif not path.startswith("/") and not path.startswith("http"): | |
| 57 | + path = "/" + base_path + path | |
| 52 | 58 | return f'<a href="{path}">{text}</a>' |
| 53 | 59 | |
| 54 | 60 | # Match [path | text] with flexible whitespace around the pipe |
| 55 | 61 | content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_link_replace, content) |
| 56 | 62 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"<pre><code>\1</code></pre>", content, flags=re.DOTALL) |
| @@ -143,10 +149,22 @@ | ||
| 143 | 149 | if url.startswith("/timeline"): |
| 144 | 150 | return f'href="{base}/timeline/"' |
| 145 | 151 | # /forum -> forum |
| 146 | 152 | if url.startswith("/forumpost") or url.startswith("/forum"): |
| 147 | 153 | return f'href="{base}/forum/"' |
| 154 | + # /www/file.wiki or /www/subdir/file -> doc page viewer | |
| 155 | + m = re.match(r"/(www/.+)", url) | |
| 156 | + if m: | |
| 157 | + return f'href="{base}/docs/{m.group(1)}"' | |
| 158 | + # /help/command -> Fossil help (link to fossil docs) | |
| 159 | + m = re.match(r"/help/(.+)", url) | |
| 160 | + if m: | |
| 161 | + return f'href="{base}/docs/www/help.wiki"' | |
| 162 | + # Bare .wiki or .md file paths (from relative link resolution) | |
| 163 | + m = re.match(r"/([^/]+\.(?:wiki|md|html))", url) | |
| 164 | + if m: | |
| 165 | + return f'href="{base}/docs/www/{m.group(1)}"' | |
| 148 | 166 | # Keep external and unrecognized links as-is |
| 149 | 167 | return match.group(0) |
| 150 | 168 | |
| 151 | 169 | def replace_scheme_link(match): |
| 152 | 170 | """Handle Fossil URI schemes like forum:/forumpost/HASH, wiki:PageName, info:HASH.""" |
| @@ -767,11 +785,15 @@ | ||
| 767 | 785 | try: |
| 768 | 786 | content = content_bytes.decode("utf-8") |
| 769 | 787 | except UnicodeDecodeError as e: |
| 770 | 788 | raise Http404("Binary file cannot be rendered as documentation") from e |
| 771 | 789 | |
| 772 | - content_html = mark_safe(_render_fossil_content(content, project_slug=slug)) | |
| 790 | + # Compute base_path for relative link resolution (e.g. "www/" for "www/concepts.wiki") | |
| 791 | + doc_base = "/".join(doc_path.split("/")[:-1]) | |
| 792 | + if doc_base: | |
| 793 | + doc_base += "/" | |
| 794 | + content_html = mark_safe(_render_fossil_content(content, project_slug=slug, base_path=doc_base)) | |
| 773 | 795 | |
| 774 | 796 | return render( |
| 775 | 797 | request, |
| 776 | 798 | "fossil/doc_page.html", |
| 777 | 799 | {"project": project, "doc_path": doc_path, "content_html": content_html, "active_tab": "wiki"}, |
| 778 | 800 |
| --- fossil/views.py | |
| +++ fossil/views.py | |
| @@ -11,17 +11,19 @@ | |
| 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> |
| 22 | - Markdown (newer pages) |
| 23 | """ |
| 24 | if not content: |
| 25 | return "" |
| 26 | |
| 27 | # Detect format from the raw content BEFORE any transformations |
| @@ -31,11 +33,13 @@ | |
| 31 | # Markdown: convert Fossil [path | text] links to markdown links first |
| 32 | def _fossil_to_md_link(m): |
| 33 | path = m.group(1).strip() |
| 34 | text = m.group(2).strip() |
| 35 | if path.startswith("./"): |
| 36 | path = "/" + path[2:] |
| 37 | return f"[{text}]({path})" |
| 38 | |
| 39 | content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_to_md_link, content) |
| 40 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"```\n\1\n```", content, flags=re.DOTALL) |
| 41 | html = md.markdown(content, extensions=["fenced_code", "tables", "toc"]) |
| @@ -44,13 +48,15 @@ | |
| 44 | # Fossil wiki / HTML: convert Fossil-specific syntax to HTML |
| 45 | # Fossil links: [path | text] or [path|text] — spaces around pipe are optional |
| 46 | def _fossil_link_replace(match): |
| 47 | path = match.group(1).strip() |
| 48 | text = match.group(2).strip() |
| 49 | # Convert relative paths (./foo) to absolute (/foo) |
| 50 | if path.startswith("./"): |
| 51 | path = "/" + path[2:] |
| 52 | return f'<a href="{path}">{text}</a>' |
| 53 | |
| 54 | # Match [path | text] with flexible whitespace around the pipe |
| 55 | content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_link_replace, content) |
| 56 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"<pre><code>\1</code></pre>", content, flags=re.DOTALL) |
| @@ -143,10 +149,22 @@ | |
| 143 | if url.startswith("/timeline"): |
| 144 | return f'href="{base}/timeline/"' |
| 145 | # /forum -> forum |
| 146 | if url.startswith("/forumpost") or url.startswith("/forum"): |
| 147 | return f'href="{base}/forum/"' |
| 148 | # Keep external and unrecognized links as-is |
| 149 | return match.group(0) |
| 150 | |
| 151 | def replace_scheme_link(match): |
| 152 | """Handle Fossil URI schemes like forum:/forumpost/HASH, wiki:PageName, info:HASH.""" |
| @@ -767,11 +785,15 @@ | |
| 767 | try: |
| 768 | content = content_bytes.decode("utf-8") |
| 769 | except UnicodeDecodeError as e: |
| 770 | raise Http404("Binary file cannot be rendered as documentation") from e |
| 771 | |
| 772 | content_html = mark_safe(_render_fossil_content(content, project_slug=slug)) |
| 773 | |
| 774 | return render( |
| 775 | request, |
| 776 | "fossil/doc_page.html", |
| 777 | {"project": project, "doc_path": doc_path, "content_html": content_html, "active_tab": "wiki"}, |
| 778 |
| --- fossil/views.py | |
| +++ fossil/views.py | |
| @@ -11,17 +11,19 @@ | |
| 11 | |
| 12 | from .models import FossilRepository |
| 13 | from .reader import FossilReader |
| 14 | |
| 15 | |
| 16 | def _render_fossil_content(content: str, project_slug: str = "", base_path: 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> |
| 22 | - Markdown (newer pages) |
| 23 | |
| 24 | base_path: directory of the current file (e.g. "www/") for resolving relative links. |
| 25 | """ |
| 26 | if not content: |
| 27 | return "" |
| 28 | |
| 29 | # Detect format from the raw content BEFORE any transformations |
| @@ -31,11 +33,13 @@ | |
| 33 | # Markdown: convert Fossil [path | text] links to markdown links first |
| 34 | def _fossil_to_md_link(m): |
| 35 | path = m.group(1).strip() |
| 36 | text = m.group(2).strip() |
| 37 | if path.startswith("./"): |
| 38 | path = "/" + base_path + path[2:] |
| 39 | elif not path.startswith("/") and not path.startswith("http"): |
| 40 | path = "/" + base_path + path |
| 41 | return f"[{text}]({path})" |
| 42 | |
| 43 | content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_to_md_link, content) |
| 44 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"```\n\1\n```", content, flags=re.DOTALL) |
| 45 | html = md.markdown(content, extensions=["fenced_code", "tables", "toc"]) |
| @@ -44,13 +48,15 @@ | |
| 48 | # Fossil wiki / HTML: convert Fossil-specific syntax to HTML |
| 49 | # Fossil links: [path | text] or [path|text] — spaces around pipe are optional |
| 50 | def _fossil_link_replace(match): |
| 51 | path = match.group(1).strip() |
| 52 | text = match.group(2).strip() |
| 53 | # Convert relative paths to absolute using base_path |
| 54 | if path.startswith("./"): |
| 55 | path = "/" + base_path + path[2:] |
| 56 | elif not path.startswith("/") and not path.startswith("http"): |
| 57 | path = "/" + base_path + path |
| 58 | return f'<a href="{path}">{text}</a>' |
| 59 | |
| 60 | # Match [path | text] with flexible whitespace around the pipe |
| 61 | content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_link_replace, content) |
| 62 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"<pre><code>\1</code></pre>", content, flags=re.DOTALL) |
| @@ -143,10 +149,22 @@ | |
| 149 | if url.startswith("/timeline"): |
| 150 | return f'href="{base}/timeline/"' |
| 151 | # /forum -> forum |
| 152 | if url.startswith("/forumpost") or url.startswith("/forum"): |
| 153 | return f'href="{base}/forum/"' |
| 154 | # /www/file.wiki or /www/subdir/file -> doc page viewer |
| 155 | m = re.match(r"/(www/.+)", url) |
| 156 | if m: |
| 157 | return f'href="{base}/docs/{m.group(1)}"' |
| 158 | # /help/command -> Fossil help (link to fossil docs) |
| 159 | m = re.match(r"/help/(.+)", url) |
| 160 | if m: |
| 161 | return f'href="{base}/docs/www/help.wiki"' |
| 162 | # Bare .wiki or .md file paths (from relative link resolution) |
| 163 | m = re.match(r"/([^/]+\.(?:wiki|md|html))", url) |
| 164 | if m: |
| 165 | return f'href="{base}/docs/www/{m.group(1)}"' |
| 166 | # Keep external and unrecognized links as-is |
| 167 | return match.group(0) |
| 168 | |
| 169 | def replace_scheme_link(match): |
| 170 | """Handle Fossil URI schemes like forum:/forumpost/HASH, wiki:PageName, info:HASH.""" |
| @@ -767,11 +785,15 @@ | |
| 785 | try: |
| 786 | content = content_bytes.decode("utf-8") |
| 787 | except UnicodeDecodeError as e: |
| 788 | raise Http404("Binary file cannot be rendered as documentation") from e |
| 789 | |
| 790 | # Compute base_path for relative link resolution (e.g. "www/" for "www/concepts.wiki") |
| 791 | doc_base = "/".join(doc_path.split("/")[:-1]) |
| 792 | if doc_base: |
| 793 | doc_base += "/" |
| 794 | content_html = mark_safe(_render_fossil_content(content, project_slug=slug, base_path=doc_base)) |
| 795 | |
| 796 | return render( |
| 797 | request, |
| 798 | "fossil/doc_page.html", |
| 799 | {"project": project, "doc_path": doc_path, "content_html": content_html, "active_tab": "wiki"}, |
| 800 |