""" Python-Markdown extension: converts [[Wikilinks]] to standard Markdown links. Mirrors the behavior of mkdocs-ezlinks-plugin's WikiLinkScanner, but as a pure Markdown preprocessor (no MkDocs plugin API needed). Zensical handles the resulting .md links natively. Syntax: [[Page Name]] → [Page Name](page-name.md) [[Page Name|Display Text]] → [Display Text](page-name.md) [[Page Name#anchor]] → [Page Name](page-name.md#anchor) [[Page Name#anchor|Text]] → [Text](page-name.md#anchor) ![[image.png]] → ![image.png](image.png) [[folder/Page Name]] → [Page Name](folder/page-name.md) Code blocks (fenced and inline) are skipped — wiki links inside them pass through unchanged. """ from __future__ import annotations import re from markdown.preprocessors import Preprocessor from markdown.extensions import Extension # Regex for a wiki link. Borrowed from mkdocs-ezlinks-plugin's # WikiLinkScanner with minor adjustments. _WIKILINK_RE = re.compile( r""" (?P\!?) # optional ! for image embeds \[\[ # opening [[ (?P[^#\|\]]*?) # target (everything before #, |, or ]]) (?:\#(?P[^\|\]]+)?)? # optional #anchor (?:\|(?P[^\]]+)?)? # optional |display text \]\] # closing ]] """, re.VERBOSE, ) # Match fenced code blocks or inline code spans (single capture group). _CODE_RE = re.compile( r"(```.*?```|`[^`\n]+`)", re.DOTALL, ) def _slugify(text: str) -> str: """Convert a human page name into a filename slug. Mirrors ezlinks: lowercase, spaces → hyphens, strip unsupported chars. Keeps forward slashes for subdirectory paths, dots for extensions, and CJK. """ slug = text.strip().lower() slug = re.sub(r"\s+", "-", slug) # Keep: word chars, hyphens, dots, forward slashes, CJK slug = re.sub(r"[^\w./\u4e00-\u9fff\-]", "", slug) return slug class WikilinksPreprocessor(Preprocessor): """Preprocessor that converts [[wikilinks]] to [text](slug.md).""" def run(self, lines: list[str]) -> list[str]: text = "\n".join(lines) # Walk through the text, skipping code spans. Use finditer to # extract code regions, then transform wiki links in the gaps. result: list[str] = [] cursor = 0 for m in _CODE_RE.finditer(text): # Transform the non-code region before this match before = text[cursor : m.start()] if before: result.append(_WIKILINK_RE.sub(self._replace, before)) # Pass through the code region unchanged result.append(m.group(0)) cursor = m.end() # Transform any remaining text after the last code region tail = text[cursor:] if tail: result.append(_WIKILINK_RE.sub(self._replace, tail)) return "\n".join(result).split("\n") def _replace(self, match: re.Match) -> str: groups = match.groupdict() is_image = groups.get("image") or "" link = groups.get("link") or "" anchor = groups.get("anchor") or "" text = groups.get("text") or link or anchor if not (link or text or anchor): return match.group(0) slug = _slugify(link) if link else "" href = slug if anchor: anchor_slug = _slugify(anchor) if anchor_slug: href = f"{slug}#{anchor_slug}" if slug else f"#{anchor_slug}" if not href: return match.group(0) # Build standard markdown link / image if is_image: return f"![{text}]({href})" else: return f"[{text}]({href})" class WikilinksExtension(Extension): """Register the WikilinksPreprocessor.""" def extendMarkdown(self, md): # Priority 35: after normalize_whitespace (30), before html_block (20) md.preprocessors.register(WikilinksPreprocessor(md), "wikilinks", 35) def makeExtension(**kwargs): return WikilinksExtension(**kwargs)