feat: add wiki link support ([[page]]) via custom Python-Markdown extension
Some checks failed
Deploy Docs / deploy (push) Failing after 24s
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:
parent
d1f405bde1
commit
69119f347d
8 changed files with 160 additions and 26 deletions
|
|
@ -17,6 +17,7 @@ jobs:
|
||||||
. /tmp/venv/bin/activate
|
. /tmp/venv/bin/activate
|
||||||
python3 --version
|
python3 --version
|
||||||
pip3 install zensical
|
pip3 install zensical
|
||||||
|
pip3 install -e .
|
||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
. /tmp/venv/bin/activate
|
. /tmp/venv/bin/activate
|
||||||
|
|
|
||||||
0
_extensions/__init__.py
Normal file
0
_extensions/__init__.py
Normal file
123
_extensions/wikilinks.py
Normal file
123
_extensions/wikilinks.py
Normal 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]] → 
|
||||||
|
[[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)
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
---
|
---
|
||||||
icon: simple/markdown
|
icon: simple/markdown
|
||||||
---
|
---
|
||||||
|
|
||||||
# Markdown in 5min
|
# 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
|
## Headers
|
||||||
|
|
||||||
```
|
```text
|
||||||
# H1 Header
|
# H1 Header
|
||||||
## H2 Header
|
## H2 Header
|
||||||
### H3 Header
|
### H3 Header
|
||||||
|
|
@ -17,7 +20,7 @@ icon: simple/markdown
|
||||||
|
|
||||||
## Text formatting
|
## Text formatting
|
||||||
|
|
||||||
```
|
```text
|
||||||
**bold text**
|
**bold text**
|
||||||
*italic text*
|
*italic text*
|
||||||
***bold and italic***
|
***bold and italic***
|
||||||
|
|
@ -27,16 +30,16 @@ icon: simple/markdown
|
||||||
|
|
||||||
## Links and images
|
## Links and images
|
||||||
|
|
||||||
```
|
```text
|
||||||
[Link text](https://example.com)
|
[Link text](https://example.com)
|
||||||
[Link with title](https://example.com "Hover title")
|
[Link with title](https://example.com "Hover title")
|
||||||

|

|
||||||

|

|
||||||
```
|
```
|
||||||
|
|
||||||
## Lists
|
## Lists
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
Unordered:
|
Unordered:
|
||||||
|
|
||||||
- Item 1
|
- Item 1
|
||||||
|
|
@ -52,7 +55,7 @@ Ordered:
|
||||||
|
|
||||||
## Blockquotes
|
## Blockquotes
|
||||||
|
|
||||||
```
|
```text
|
||||||
> This is a blockquote
|
> This is a blockquote
|
||||||
> Multiple lines
|
> Multiple lines
|
||||||
>> Nested quote
|
>> Nested quote
|
||||||
|
|
@ -60,7 +63,7 @@ Ordered:
|
||||||
|
|
||||||
## Code blocks
|
## Code blocks
|
||||||
|
|
||||||
````
|
````javascript
|
||||||
```javascript
|
```javascript
|
||||||
function hello() {
|
function hello() {
|
||||||
console.log("Hello, world!");
|
console.log("Hello, world!");
|
||||||
|
|
@ -70,7 +73,7 @@ function hello() {
|
||||||
|
|
||||||
## Tables
|
## Tables
|
||||||
|
|
||||||
```
|
```text
|
||||||
| Header 1 | Header 2 | Header 3 |
|
| Header 1 | Header 2 | Header 3 |
|
||||||
|----------|----------|----------|
|
|----------|----------|----------|
|
||||||
| Row 1 | Data | Data |
|
| Row 1 | Data | Data |
|
||||||
|
|
@ -79,7 +82,7 @@ function hello() {
|
||||||
|
|
||||||
## Horizontal rule
|
## Horizontal rule
|
||||||
|
|
||||||
```
|
```text
|
||||||
---
|
---
|
||||||
or
|
or
|
||||||
***
|
***
|
||||||
|
|
@ -89,7 +92,7 @@ ___
|
||||||
|
|
||||||
## Task lists
|
## Task lists
|
||||||
|
|
||||||
```
|
```text
|
||||||
- [x] Completed task
|
- [x] Completed task
|
||||||
- [ ] Incomplete task
|
- [ ] Incomplete task
|
||||||
- [ ] Another task
|
- [ ] Another task
|
||||||
|
|
@ -97,13 +100,13 @@ ___
|
||||||
|
|
||||||
## Escaping characters
|
## Escaping characters
|
||||||
|
|
||||||
```
|
```text
|
||||||
Use backslash to escape: \* \_ \# \`
|
Use backslash to escape: \* \_ \# \`
|
||||||
```
|
```
|
||||||
|
|
||||||
## Line breaks
|
## Line breaks
|
||||||
|
|
||||||
```
|
```text
|
||||||
End a line with two spaces
|
End a line with two spaces
|
||||||
to create a line break.
|
to create a line break.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
---
|
---
|
||||||
icon: material/spa
|
icon: material/spa
|
||||||
---
|
---
|
||||||
|
|
||||||
# Spiritual Hygiene
|
# 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.
|
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
|
## 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
|
- 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)
|
- Burn natural incense or diffuse pure essential oils (frankincense, sage, palo santo)
|
||||||
|
|
||||||
??? question "How often should I cleanse?"
|
??? 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.
|
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
|
## 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.
|
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
|
## 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.
|
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
|
## Group singing, toning, and joy
|
||||||
|
|
||||||
|
|
@ -58,7 +57,7 @@ Pair this with:
|
||||||
|
|
||||||
"Joy maxxing" is not avoidance. It is deliberate nervous system nourishment.
|
"Joy maxxing" is not avoidance. It is deliberate nervous system nourishment.
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## Mass meditations and peace visualization
|
## 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.
|
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.
|
**Related:** [Daily Sadhana](daily-sadhana.md) — spiritual hygiene as daily practice.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "up"
|
name = "up"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -6,6 +10,9 @@ readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["_extensions"]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"zensical>=0.0.43",
|
"zensical>=0.0.43",
|
||||||
|
|
|
||||||
2
uv.lock
2
uv.lock
|
|
@ -232,7 +232,7 @@ wheels = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "up"
|
name = "up"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { editable = "." }
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
|
|
||||||
|
|
@ -332,6 +332,7 @@ lang = "en"
|
||||||
[project.markdown_extensions.md_in_html]
|
[project.markdown_extensions.md_in_html]
|
||||||
[project.markdown_extensions.toc]
|
[project.markdown_extensions.toc]
|
||||||
permalink = true
|
permalink = true
|
||||||
|
[project.markdown_extensions."_extensions.wikilinks:WikilinksExtension"]
|
||||||
[project.markdown_extensions.pymdownx.arithmatex]
|
[project.markdown_extensions.pymdownx.arithmatex]
|
||||||
generic = true
|
generic = true
|
||||||
[project.markdown_extensions.pymdownx.betterem]
|
[project.markdown_extensions.pymdownx.betterem]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue