feat: add wiki link support ([[page]]) via custom Python-Markdown extension
Some checks failed
Deploy Docs / deploy (push) Failing after 24s

- 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
This commit is contained in:
Tolaria 2026-06-03 20:07:25 +08:00
parent d1f405bde1
commit 69119f347d
8 changed files with 160 additions and 26 deletions

View file

@ -17,6 +17,7 @@ jobs:
. /tmp/venv/bin/activate
python3 --version
pip3 install zensical
pip3 install -e .
- run: |
. /tmp/venv/bin/activate

0
_extensions/__init__.py Normal file
View file

123
_extensions/wikilinks.py Normal file
View file

@ -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<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"![{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)

View file

@ -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,13 +100,13 @@ ___
## Escaping characters
```
```text
Use backslash to escape: \* \_ \# \`
```
## Line breaks
```
```text
End a line with two spaces
to create a line break.

View file

@ -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,7 +67,7 @@ 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.

View file

@ -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",

View file

@ -232,7 +232,7 @@ wheels = [
[[package]]
name = "up"
version = "0.1.0"
source = { virtual = "." }
source = { editable = "." }
[package.dev-dependencies]
dev = [

View file

@ -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]