From 69119f347de8ca1b42fb57259457247f9eb41f39 Mon Sep 17 00:00:00 2001 From: Tolaria Date: Wed, 3 Jun 2026 20:07:25 +0800 Subject: [PATCH] feat: add wiki link support ([[page]]) via custom Python-Markdown extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New package _extensions/wikilinks.py: preprocessor that converts [[Page Name]], [[Page|Text]], [[Page#anchor|Text]], and ![[img]] to standard Markdown links before parsing - Skips wiki links inside fenced/inline code blocks - Slugifies targets: lowercase, spaces→hyphens - Registered as markdown extension in zensical.toml - Updated CI workflow to pip install -e . for the _extensions package - First usage: [[docs/contributing/index]] in markdown.md --- .forgejo/workflows/docs.yml | 1 + _extensions/__init__.py | 0 _extensions/wikilinks.py | 123 ++++++++++++++++++++++++++++ docs/contributing/markdown.md | 33 ++++---- docs/practices/spiritual-hygiene.md | 19 ++--- pyproject.toml | 7 ++ uv.lock | 2 +- zensical.toml | 1 + 8 files changed, 160 insertions(+), 26 deletions(-) create mode 100644 _extensions/__init__.py create mode 100644 _extensions/wikilinks.py diff --git a/.forgejo/workflows/docs.yml b/.forgejo/workflows/docs.yml index 488acd0..6f34b57 100644 --- a/.forgejo/workflows/docs.yml +++ b/.forgejo/workflows/docs.yml @@ -17,6 +17,7 @@ jobs: . /tmp/venv/bin/activate python3 --version pip3 install zensical + pip3 install -e . - run: | . /tmp/venv/bin/activate diff --git a/_extensions/__init__.py b/_extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/_extensions/wikilinks.py b/_extensions/wikilinks.py new file mode 100644 index 0000000..e3f2821 --- /dev/null +++ b/_extensions/wikilinks.py @@ -0,0 +1,123 @@ +""" +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) diff --git a/docs/contributing/markdown.md b/docs/contributing/markdown.md index e2da6dc..0e89e33 100644 --- a/docs/contributing/markdown.md +++ b/docs/contributing/markdown.md @@ -1,12 +1,15 @@ --- icon: simple/markdown --- - # Markdown in 5min +## Introduction to Markdown Language + +Markdown is a simple language to build web pages. It is easier to read and write than HTML (the language web browsers read) and so we use it to write articles. You can use this reference to help you write posts and pages directly in our repository where the source documents for this wiki are held. See [[docs/contributing/index]] for more. + ## Headers -``` +```text # H1 Header ## H2 Header ### H3 Header @@ -17,7 +20,7 @@ icon: simple/markdown ## Text formatting -``` +```text **bold text** *italic text* ***bold and italic*** @@ -27,16 +30,16 @@ icon: simple/markdown ## Links and images -``` +```text [Link text](https://example.com) [Link with title](https://example.com "Hover title") -![Alt text](image.jpg) -![Image with title](image.jpg "Image title") +![Alt text](./image.jpg) +![Image with title](./image.jpg "Image title") ``` ## Lists -``` +```yaml Unordered: - Item 1 @@ -52,7 +55,7 @@ Ordered: ## Blockquotes -``` +```text > This is a blockquote > Multiple lines >> Nested quote @@ -60,7 +63,7 @@ Ordered: ## Code blocks -```` +````javascript ```javascript function hello() { console.log("Hello, world!"); @@ -70,7 +73,7 @@ function hello() { ## Tables -``` +```text | Header 1 | Header 2 | Header 3 | |----------|----------|----------| | Row 1 | Data | Data | @@ -79,7 +82,7 @@ function hello() { ## Horizontal rule -``` +```text --- or *** @@ -89,7 +92,7 @@ ___ ## Task lists -``` +```text - [x] Completed task - [ ] Incomplete task - [ ] Another task @@ -97,15 +100,15 @@ ___ ## Escaping characters -``` +```text Use backslash to escape: \* \_ \# \` ``` ## Line breaks -``` +```text End a line with two spaces to create a line break. Or use a blank line for a new paragraph. -``` \ No newline at end of file +``` diff --git a/docs/practices/spiritual-hygiene.md b/docs/practices/spiritual-hygiene.md index 4ab4696..104de4f 100644 --- a/docs/practices/spiritual-hygiene.md +++ b/docs/practices/spiritual-hygiene.md @@ -1,12 +1,11 @@ --- icon: material/spa --- - # Spiritual Hygiene Just as you wash your body, your space and energy field require regular cleansing. This is not superstition — it is the recognition that environments hold residue, and that your intention shapes what you encounter. ---- +*** ## Cleansing your space @@ -18,10 +17,10 @@ Your bedroom is the most important room to tend. You spend a third of your life - Use sound: a bell, singing bowl, or simple clapping in corners breaks up density - Burn natural incense or diffuse pure essential oils (frankincense, sage, palo santo) -??? question "How often should I cleanse?" - A light daily clearing (open window, intention setting) and a deeper weekly reset (sound, smoke, decluttering) is a sustainable rhythm. After arguments, illness, or visits from heavy energy, cleanse immediately. +??? question "How often should I cleanse?"\ +A light daily clearing (open window, intention setting) and a deeper weekly reset (sound, smoke, decluttering) is a sustainable rhythm. After arguments, illness, or visits from heavy energy, cleanse immediately. ---- +*** ## Setting intention and calling guides @@ -31,7 +30,7 @@ Before sleep, state clearly — aloud or internally: You don't need to believe in guides for this to work. The practice is about setting a boundary and orienting your awareness toward what is loving, clear, and true. Whatever your framework — ancestors, angels, higher self, or simply the clearest version of your own consciousness — the act of calling it in is what matters. ---- +*** ## Sound as medicine @@ -44,7 +43,7 @@ The frequencies you surround yourself with shape your nervous system. Consider: The rule is simple: if a song has a single negative word or message, skip it. The subconscious absorbs everything. Curate your sound environment as carefully as your food. ---- +*** ## Group singing, toning, and joy @@ -58,7 +57,7 @@ Pair this with: "Joy maxxing" is not avoidance. It is deliberate nervous system nourishment. ---- +*** ## Mass meditations and peace visualization @@ -68,8 +67,8 @@ Join global mass meditations when they are called. Or gather a small group — e Create something tangible to crystallize the intention: a doodle, a piece of art, a short written statement of what that positive timeline looks like *for you*. Make it specific. Make it beautiful. Keep it visible. ---- +*** **Related:** [Daily Sadhana](daily-sadhana.md) — spiritual hygiene as daily practice. -*What's the first thing you'll clear from your bedroom tonight?* \ No newline at end of file +*What's the first thing you'll clear from your bedroom tonight?* diff --git a/pyproject.toml b/pyproject.toml index 852af1d..4460d60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [project] name = "up" version = "0.1.0" @@ -6,6 +10,9 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [] +[tool.hatch.build.targets.wheel] +packages = ["_extensions"] + [dependency-groups] dev = [ "zensical>=0.0.43", diff --git a/uv.lock b/uv.lock index c791866..5bc065a 100644 --- a/uv.lock +++ b/uv.lock @@ -232,7 +232,7 @@ wheels = [ [[package]] name = "up" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } [package.dev-dependencies] dev = [ diff --git a/zensical.toml b/zensical.toml index 33ba8a6..25db022 100644 --- a/zensical.toml +++ b/zensical.toml @@ -332,6 +332,7 @@ lang = "en" [project.markdown_extensions.md_in_html] [project.markdown_extensions.toc] permalink = true +[project.markdown_extensions."_extensions.wikilinks:WikilinksExtension"] [project.markdown_extensions.pymdownx.arithmatex] generic = true [project.markdown_extensions.pymdownx.betterem]