124 lines
4.1 KiB
Python
124 lines
4.1 KiB
Python
|
|
"""
|
||
|
|
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]] → 
|
||
|
|
[[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<image>\!?) # optional ! for image embeds
|
||
|
|
\[\[ # opening [[
|
||
|
|
(?P<link>[^#\|\]]*?) # target (everything before #, |, or ]])
|
||
|
|
(?:\#(?P<anchor>[^\|\]]+)?)? # optional #anchor
|
||
|
|
(?:\|(?P<text>[^\]]+)?)? # 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""
|
||
|
|
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)
|