FossilRepo
Add Pikchr diagram rendering support - <verbatim type="pikchr">...</verbatim> in Fossil wiki → rendered SVG - ```pikchr fenced code blocks in markdown → rendered SVG - Uses `fossil pikchr` CLI for server-side rendering - Falls back to <pre><code> display if fossil binary unavailable - Wraps SVG in <div class="pikchr-diagram"> for styling
Commit
fefca89c2be51c7f5f4d83e8240c24aa8108b93e9089472bc67d6f84b1cba7cd
Parent
b311b008e340339…
2 files changed
+16
+31
+16
| --- fossil/cli.py | ||
| +++ fossil/cli.py | ||
| @@ -32,10 +32,26 @@ | ||
| 32 | 32 | try: |
| 33 | 33 | self._run("version") |
| 34 | 34 | return True |
| 35 | 35 | except (FileNotFoundError, subprocess.CalledProcessError): |
| 36 | 36 | return False |
| 37 | + | |
| 38 | + def render_pikchr(self, source: str) -> str: | |
| 39 | + """Render Pikchr markup to SVG. Returns SVG string or empty on failure.""" | |
| 40 | + try: | |
| 41 | + result = subprocess.run( | |
| 42 | + [self.binary, "pikchr", "-"], | |
| 43 | + input=source, | |
| 44 | + capture_output=True, | |
| 45 | + text=True, | |
| 46 | + timeout=10, | |
| 47 | + ) | |
| 48 | + if result.returncode == 0: | |
| 49 | + return result.stdout | |
| 50 | + except (FileNotFoundError, subprocess.TimeoutExpired): | |
| 51 | + pass | |
| 52 | + return "" | |
| 37 | 53 | |
| 38 | 54 | def wiki_commit(self, repo_path: Path, page_name: str, content: str, user: str = "") -> bool: |
| 39 | 55 | """Create or update a wiki page. Pipes content to fossil wiki commit.""" |
| 40 | 56 | cmd = [self.binary, "wiki", "commit", page_name, "-R", str(repo_path)] |
| 41 | 57 | if user: |
| 42 | 58 |
| --- fossil/cli.py | |
| +++ fossil/cli.py | |
| @@ -32,10 +32,26 @@ | |
| 32 | try: |
| 33 | self._run("version") |
| 34 | return True |
| 35 | except (FileNotFoundError, subprocess.CalledProcessError): |
| 36 | return False |
| 37 | |
| 38 | def wiki_commit(self, repo_path: Path, page_name: str, content: str, user: str = "") -> bool: |
| 39 | """Create or update a wiki page. Pipes content to fossil wiki commit.""" |
| 40 | cmd = [self.binary, "wiki", "commit", page_name, "-R", str(repo_path)] |
| 41 | if user: |
| 42 |
| --- fossil/cli.py | |
| +++ fossil/cli.py | |
| @@ -32,10 +32,26 @@ | |
| 32 | try: |
| 33 | self._run("version") |
| 34 | return True |
| 35 | except (FileNotFoundError, subprocess.CalledProcessError): |
| 36 | return False |
| 37 | |
| 38 | def render_pikchr(self, source: str) -> str: |
| 39 | """Render Pikchr markup to SVG. Returns SVG string or empty on failure.""" |
| 40 | try: |
| 41 | result = subprocess.run( |
| 42 | [self.binary, "pikchr", "-"], |
| 43 | input=source, |
| 44 | capture_output=True, |
| 45 | text=True, |
| 46 | timeout=10, |
| 47 | ) |
| 48 | if result.returncode == 0: |
| 49 | return result.stdout |
| 50 | except (FileNotFoundError, subprocess.TimeoutExpired): |
| 51 | pass |
| 52 | return "" |
| 53 | |
| 54 | def wiki_commit(self, repo_path: Path, page_name: str, content: str, user: str = "") -> bool: |
| 55 | """Create or update a wiki page. Pipes content to fossil wiki commit.""" |
| 56 | cmd = [self.binary, "wiki", "commit", page_name, "-R", str(repo_path)] |
| 57 | if user: |
| 58 |
+31
| --- fossil/views.py | ||
| +++ fossil/views.py | ||
| @@ -41,10 +41,25 @@ | ||
| 41 | 41 | return f"[{text}]({path})" |
| 42 | 42 | |
| 43 | 43 | content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_to_md_link, content) |
| 44 | 44 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"```\n\1\n```", content, flags=re.DOTALL) |
| 45 | 45 | html = md.markdown(content, extensions=["fenced_code", "tables", "toc"]) |
| 46 | + | |
| 47 | + # Post-process: render pikchr fenced code blocks to SVG | |
| 48 | + def _render_pikchr_md(m): | |
| 49 | + try: | |
| 50 | + from fossil.cli import FossilCLI | |
| 51 | + | |
| 52 | + cli = FossilCLI() | |
| 53 | + svg = cli.render_pikchr(m.group(1)) | |
| 54 | + if svg: | |
| 55 | + return f'<div class="pikchr-diagram">{svg}</div>' | |
| 56 | + except Exception: | |
| 57 | + pass | |
| 58 | + return m.group(0) | |
| 59 | + | |
| 60 | + html = re.sub(r'<code class="language-pikchr">(.*?)</code>', _render_pikchr_md, html, flags=re.DOTALL) | |
| 46 | 61 | return _rewrite_fossil_links(html, project_slug) if project_slug else html |
| 47 | 62 | |
| 48 | 63 | # Fossil wiki / HTML: convert Fossil-specific syntax to HTML |
| 49 | 64 | # Fossil links: [path | text] or [path|text] — spaces around pipe are optional |
| 50 | 65 | def _fossil_link_replace(match): |
| @@ -63,11 +78,27 @@ | ||
| 63 | 78 | content = re.sub(r"\[wikipedia:([^\]]+)\]", r'<a href="https://en.wikipedia.org/wiki/\1">\1</a>', content) |
| 64 | 79 | # Anchor links: [#anchor-name] -> local anchor |
| 65 | 80 | content = re.sub(r"\[#([^\]]+)\]", r'<a href="#\1">\1</a>', content) |
| 66 | 81 | # Bare wiki links: [PageName] (no pipe, not a URL) |
| 67 | 82 | content = re.sub(r"\[([A-Z][a-zA-Z0-9_]+)\]", r'<a href="\1">\1</a>', content) |
| 83 | + | |
| 68 | 84 | # Verbatim blocks |
| 85 | + # Pikchr diagrams: <verbatim type="pikchr">...</verbatim> → SVG | |
| 86 | + def _render_pikchr_block(m): | |
| 87 | + try: | |
| 88 | + from fossil.cli import FossilCLI | |
| 89 | + | |
| 90 | + cli = FossilCLI() | |
| 91 | + svg = cli.render_pikchr(m.group(1)) | |
| 92 | + if svg: | |
| 93 | + return f'<div class="pikchr-diagram">{svg}</div>' | |
| 94 | + except Exception: | |
| 95 | + pass | |
| 96 | + return f'<pre><code class="language-pikchr">{m.group(1)}</code></pre>' | |
| 97 | + | |
| 98 | + content = re.sub(r'<verbatim\s+type="pikchr">(.*?)</verbatim>', _render_pikchr_block, content, flags=re.DOTALL) | |
| 99 | + # Regular verbatim blocks | |
| 69 | 100 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"<pre><code>\1</code></pre>", content, flags=re.DOTALL) |
| 70 | 101 | # <nowiki> blocks — strip the tags, content passes through as-is |
| 71 | 102 | content = re.sub(r"<nowiki>(.*?)</nowiki>", r"\1", content, flags=re.DOTALL) |
| 72 | 103 | |
| 73 | 104 | # Convert Fossil wiki list syntax: * bullets and # enumeration |
| 74 | 105 |
| --- fossil/views.py | |
| +++ fossil/views.py | |
| @@ -41,10 +41,25 @@ | |
| 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"]) |
| 46 | return _rewrite_fossil_links(html, project_slug) if project_slug else html |
| 47 | |
| 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): |
| @@ -63,11 +78,27 @@ | |
| 63 | content = re.sub(r"\[wikipedia:([^\]]+)\]", r'<a href="https://en.wikipedia.org/wiki/\1">\1</a>', content) |
| 64 | # Anchor links: [#anchor-name] -> local anchor |
| 65 | content = re.sub(r"\[#([^\]]+)\]", r'<a href="#\1">\1</a>', content) |
| 66 | # Bare wiki links: [PageName] (no pipe, not a URL) |
| 67 | content = re.sub(r"\[([A-Z][a-zA-Z0-9_]+)\]", r'<a href="\1">\1</a>', content) |
| 68 | # Verbatim blocks |
| 69 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"<pre><code>\1</code></pre>", content, flags=re.DOTALL) |
| 70 | # <nowiki> blocks — strip the tags, content passes through as-is |
| 71 | content = re.sub(r"<nowiki>(.*?)</nowiki>", r"\1", content, flags=re.DOTALL) |
| 72 | |
| 73 | # Convert Fossil wiki list syntax: * bullets and # enumeration |
| 74 |
| --- fossil/views.py | |
| +++ fossil/views.py | |
| @@ -41,10 +41,25 @@ | |
| 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"]) |
| 46 | |
| 47 | # Post-process: render pikchr fenced code blocks to SVG |
| 48 | def _render_pikchr_md(m): |
| 49 | try: |
| 50 | from fossil.cli import FossilCLI |
| 51 | |
| 52 | cli = FossilCLI() |
| 53 | svg = cli.render_pikchr(m.group(1)) |
| 54 | if svg: |
| 55 | return f'<div class="pikchr-diagram">{svg}</div>' |
| 56 | except Exception: |
| 57 | pass |
| 58 | return m.group(0) |
| 59 | |
| 60 | html = re.sub(r'<code class="language-pikchr">(.*?)</code>', _render_pikchr_md, html, flags=re.DOTALL) |
| 61 | return _rewrite_fossil_links(html, project_slug) if project_slug else html |
| 62 | |
| 63 | # Fossil wiki / HTML: convert Fossil-specific syntax to HTML |
| 64 | # Fossil links: [path | text] or [path|text] — spaces around pipe are optional |
| 65 | def _fossil_link_replace(match): |
| @@ -63,11 +78,27 @@ | |
| 78 | content = re.sub(r"\[wikipedia:([^\]]+)\]", r'<a href="https://en.wikipedia.org/wiki/\1">\1</a>', content) |
| 79 | # Anchor links: [#anchor-name] -> local anchor |
| 80 | content = re.sub(r"\[#([^\]]+)\]", r'<a href="#\1">\1</a>', content) |
| 81 | # Bare wiki links: [PageName] (no pipe, not a URL) |
| 82 | content = re.sub(r"\[([A-Z][a-zA-Z0-9_]+)\]", r'<a href="\1">\1</a>', content) |
| 83 | |
| 84 | # Verbatim blocks |
| 85 | # Pikchr diagrams: <verbatim type="pikchr">...</verbatim> → SVG |
| 86 | def _render_pikchr_block(m): |
| 87 | try: |
| 88 | from fossil.cli import FossilCLI |
| 89 | |
| 90 | cli = FossilCLI() |
| 91 | svg = cli.render_pikchr(m.group(1)) |
| 92 | if svg: |
| 93 | return f'<div class="pikchr-diagram">{svg}</div>' |
| 94 | except Exception: |
| 95 | pass |
| 96 | return f'<pre><code class="language-pikchr">{m.group(1)}</code></pre>' |
| 97 | |
| 98 | content = re.sub(r'<verbatim\s+type="pikchr">(.*?)</verbatim>', _render_pikchr_block, content, flags=re.DOTALL) |
| 99 | # Regular verbatim blocks |
| 100 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"<pre><code>\1</code></pre>", content, flags=re.DOTALL) |
| 101 | # <nowiki> blocks — strip the tags, content passes through as-is |
| 102 | content = re.sub(r"<nowiki>(.*?)</nowiki>", r"\1", content, flags=re.DOTALL) |
| 103 | |
| 104 | # Convert Fossil wiki list syntax: * bullets and # enumeration |
| 105 |