tashwiki 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tashwiki-0.1.1/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright 2025 Alexey Ryabov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the “Software”), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.3
2
+ Name: tashwiki
3
+ Version: 0.1.1
4
+ Summary: Wiki-like static site generator
5
+ License: MIT
6
+ Keywords: wiki,git,markdown
7
+ Author: Alexey Ryabov
8
+ Author-email: alex@b00bl1k.ru
9
+ Requires-Python: >=3.12,<4.0
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Text Processing :: Markup
13
+ Classifier: Topic :: Text Processing :: Markup :: Markdown
14
+ Requires-Dist: click (>=8.2.1,<9.0.0)
15
+ Requires-Dist: jinja2 (>=3.1.6,<4.0.0)
16
+ Requires-Dist: livereload (>=2.7.1,<3.0.0)
17
+ Requires-Dist: markdown (>=3.8.2,<4.0.0)
18
+ Requires-Dist: termcolor (>=3.1.0,<4.0.0)
19
+ Project-URL: Issues, https://github.com/b00bl1k/tashwiki/issues
20
+ Project-URL: Repository, https://github.com/b00bl1k/tashwiki
21
+ Description-Content-Type: text/markdown
22
+
23
+ # TashWiki
24
+
25
+ TashWiki is a wiki-like static site generator. It can be used to make your own
26
+ digital garden or simple personal wiki.
27
+
28
+ ## Features
29
+
30
+ - Converts your Markdown notes into static HTML pages
31
+ - Wiki-style links between pages
32
+ - Simple and lightweight
33
+ - Easy to deploy
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install git+https://github.com/b00bl1k/tashwiki.git
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ ```bash
44
+ tashwiki build
45
+ ```
46
+
47
+ This will take your source files from ./content and generate a static site
48
+ into ./output.
49
+
50
+ ## Example
51
+
52
+ Here is a minimal note:
53
+
54
+ ```markdown
55
+ # Welcome to TashWiki
56
+
57
+ This is my digital garden.
58
+
59
+ See also [[Another note]].
60
+ ```
61
+
62
+ ## Roadmap
63
+
64
+ - [x] Categories
65
+ - [ ] Themes
66
+ - [ ] Search
67
+
68
+ ## Status
69
+
70
+ TashWiki is still in early development. Expect breaking changes.
71
+
72
+ ## License
73
+
74
+ MIT
75
+
76
+ ## Credits
77
+
78
+ - [[Rock icons created by Freepik - Flaticon]](https://www.flaticon.com/free-icons/rock)
79
+
@@ -0,0 +1,56 @@
1
+ # TashWiki
2
+
3
+ TashWiki is a wiki-like static site generator. It can be used to make your own
4
+ digital garden or simple personal wiki.
5
+
6
+ ## Features
7
+
8
+ - Converts your Markdown notes into static HTML pages
9
+ - Wiki-style links between pages
10
+ - Simple and lightweight
11
+ - Easy to deploy
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install git+https://github.com/b00bl1k/tashwiki.git
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ tashwiki build
23
+ ```
24
+
25
+ This will take your source files from ./content and generate a static site
26
+ into ./output.
27
+
28
+ ## Example
29
+
30
+ Here is a minimal note:
31
+
32
+ ```markdown
33
+ # Welcome to TashWiki
34
+
35
+ This is my digital garden.
36
+
37
+ See also [[Another note]].
38
+ ```
39
+
40
+ ## Roadmap
41
+
42
+ - [x] Categories
43
+ - [ ] Themes
44
+ - [ ] Search
45
+
46
+ ## Status
47
+
48
+ TashWiki is still in early development. Expect breaking changes.
49
+
50
+ ## License
51
+
52
+ MIT
53
+
54
+ ## Credits
55
+
56
+ - [[Rock icons created by Freepik - Flaticon]](https://www.flaticon.com/free-icons/rock)
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "tashwiki"
3
+ version = "0.1.1"
4
+ description = "Wiki-like static site generator"
5
+ authors = [
6
+ {name = "Alexey Ryabov", email = "alex@b00bl1k.ru"}
7
+ ]
8
+ requires-python = ">=3.12,<4.0"
9
+ dependencies = [
10
+ "markdown (>=3.8.2,<4.0.0)",
11
+ "jinja2 (>=3.1.6,<4.0.0)",
12
+ "click (>=8.2.1,<9.0.0)",
13
+ "livereload (>=2.7.1,<3.0.0)",
14
+ "termcolor (>=3.1.0,<4.0.0)"
15
+ ]
16
+ license = "MIT"
17
+ license-files = ["LICENSE"]
18
+ readme = "README.md"
19
+ keywords = ["wiki", "git", "markdown"]
20
+ classifiers = [
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Topic :: Text Processing :: Markup",
24
+ "Topic :: Text Processing :: Markup :: Markdown",
25
+ ]
26
+
27
+ [project.scripts]
28
+ tashwiki = "tashwiki:cli.main"
29
+
30
+ [project.urls]
31
+ Repository = "https://github.com/b00bl1k/tashwiki"
32
+ Issues = "https://github.com/b00bl1k/tashwiki/issues"
33
+
34
+ [build-system]
35
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
36
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__= version(__package__)
@@ -0,0 +1,168 @@
1
+
2
+ import logging
3
+ from pathlib import Path
4
+ from shutil import copytree
5
+ from importlib import resources
6
+
7
+ from markdown import Markdown
8
+ from jinja2 import Environment, PackageLoader, TemplateNotFound
9
+ from termcolor import cprint
10
+
11
+ from tashwiki.categories import Categories, Category
12
+ from tashwiki.config import Config
13
+ from tashwiki.utils import page_name_to_label, label_to_page_name
14
+ from tashwiki.wikilinks import WikiLinkExtension
15
+
16
+ logger = logging.getLogger()
17
+
18
+
19
+ class Builder:
20
+ DEFAULT_PAGE_TEMPLATE = "page.html"
21
+ CATEGORIES_PAGE_TEMPLATE = "categories.html"
22
+ CATEGORY_TEMPLATE = "category.html"
23
+
24
+ def __init__(self, config: Config):
25
+ self.conf = config
26
+ self.source_dir = Path(config.site_source_dir)
27
+ self.output_dir = Path(config.site_output_dir)
28
+ self.glob_context = self._prepare_context()
29
+ self.env = Environment(
30
+ loader=PackageLoader("tashwiki"),
31
+ )
32
+ self.categories = Categories(config.site_category_page)
33
+ self.md = Markdown(extensions=[
34
+ "meta",
35
+ "fenced_code",
36
+ WikiLinkExtension(
37
+ base_url=config.site_baseurl,
38
+ end_url=".html",
39
+ html_class=self._build_link_class,
40
+ )
41
+ ])
42
+
43
+ def _prepare_context(self) -> dict:
44
+ return {
45
+ "site_author": self.conf.site_author,
46
+ "site_name": self.conf.site_name,
47
+ "baseurl": self.conf.site_baseurl,
48
+ "language": self.conf.site_language,
49
+ "main_page": self.conf.site_main_page,
50
+ "categories_page": self.conf.site_categories_page,
51
+ "categories_url": label_to_page_name(self.conf.site_categories_page) + ".html",
52
+ "category_page": self.conf.site_categories_page,
53
+ "category_url": label_to_page_name(self.conf.site_categories_page) + ".html",
54
+ }
55
+
56
+ def _build_link_class(self, page: str):
57
+ """Return css class for link."""
58
+
59
+ page_name = label_to_page_name(page)
60
+ page_path = (self.source_dir / page_name).with_suffix(".md")
61
+ if not page_path.exists():
62
+ cprint(f"Missing page '{page_name}'", color="red")
63
+ return "notfound"
64
+ return ""
65
+
66
+ def _render_html_page(self, out_path: Path, template: str, context: dict):
67
+ """Render page using Jinja template engine and write to output file."""
68
+
69
+ template = self.env.get_template(template)
70
+ html = template.render(context)
71
+ out_path.write_text(html, encoding="utf-8")
72
+
73
+ def _validate_meta(self, meta: dict) -> dict:
74
+ """Validate meta information from Markdown document."""
75
+
76
+ for key, value in meta.items():
77
+ if key in ("title", "author"):
78
+ meta[key] = value[0]
79
+ elif key == "template":
80
+ tpl = value[0]
81
+ try:
82
+ env.get_template(tpl)
83
+ meta[key] = tpl
84
+ except TemplateNotFound:
85
+ raise ValueError(f"Template '{tpl}' not found.")
86
+ elif key == "categories":
87
+ pass
88
+ else:
89
+ raise ValueError(f"Unknown meta field '{key}'.")
90
+ return meta
91
+
92
+ def _copy_static(self):
93
+ with resources.as_file(resources.files("tashwiki.static")) as src_path:
94
+ copytree(src_path, self.output_dir, dirs_exist_ok=True)
95
+
96
+ static_dir = Path(self.conf.site_static_dir)
97
+ if static_dir.exists():
98
+ copytree(static_dir, self.output_dir, dirs_exist_ok=True)
99
+
100
+ def build(self):
101
+ """Build site to output folder."""
102
+
103
+ print(f"Building site to folder '{self.output_dir}'...")
104
+ self.output_dir.mkdir(parents=True, exist_ok=True)
105
+
106
+ for md_file in self.source_dir.rglob("*.md"):
107
+ print(f"Rendering '{md_file.stem}'...")
108
+ self.render_page(md_file)
109
+
110
+ for category in self.categories:
111
+ print(f"Rendering category page '{category.name}'...")
112
+ self.render_category_page(category)
113
+
114
+ print("Rendering categories index page...")
115
+ self.render_categories_index()
116
+
117
+ print("Copying static files...")
118
+ self._copy_static()
119
+
120
+ def render_page(self, md_file: Path):
121
+ """Convert Markdown document to HTML page."""
122
+
123
+ page_content = self.md.convert(md_file.read_text(encoding="utf-8"))
124
+ page_name = md_file.stem
125
+ page_title = page_name_to_label(page_name)
126
+
127
+ # prepare context for rendering
128
+ page_context = {
129
+ "page_content": page_content,
130
+ "title": page_title,
131
+ }
132
+ page_context.update(self.glob_context)
133
+
134
+ # document can contain some meta information
135
+ meta = self._validate_meta(self.md.Meta)
136
+ page_template = meta.pop("template", self.DEFAULT_PAGE_TEMPLATE)
137
+ category_names = meta.pop("categories", [])
138
+ page_context.update(meta)
139
+
140
+ # extract categories from meta and update context
141
+ page_categories = []
142
+ for category_name in category_names:
143
+ category = self.categories.get_or_create(category_name)
144
+ category.add_page(page_title)
145
+ page_categories.append(category)
146
+ page_context.update({"categories": page_categories})
147
+
148
+ # actual render to a file
149
+ out_path = self.output_dir / md_file.with_suffix(".html").name
150
+ self._render_html_page(out_path, page_template, page_context)
151
+
152
+ def render_categories_index(self):
153
+ categories_context = {
154
+ "categories": list(self.categories),
155
+ "title": self.conf.site_categories_page,
156
+ }
157
+ categories_context.update(self.glob_context)
158
+ out_path = (self.output_dir / self.conf.site_categories_page).with_suffix(".html")
159
+ self._render_html_page(out_path, self.CATEGORIES_PAGE_TEMPLATE, categories_context)
160
+
161
+ def render_category_page(self, category: Category):
162
+ category_context = {
163
+ "category": category,
164
+ "title": f"{self.conf.site_category_page}: {category.name}"
165
+ }
166
+ category_context.update(self.glob_context)
167
+ out_path = self.output_dir / category.url
168
+ self._render_html_page(out_path, self.CATEGORY_TEMPLATE, category_context)
@@ -0,0 +1,54 @@
1
+ from tashwiki.utils import label_to_page_name
2
+
3
+
4
+ class Category:
5
+
6
+ def __init__(self, categories, name):
7
+ self.categories = categories
8
+ self.name = name
9
+ self.url = "{}_{}.html".format(
10
+ self.categories.basename,
11
+ label_to_page_name(name),
12
+ )
13
+ self.pages = {}
14
+
15
+ @property
16
+ def pages_count(self):
17
+ return len(self.pages)
18
+
19
+ def add_page(self, name):
20
+ if name not in self.pages:
21
+ self.pages[name] = label_to_page_name(name) + ".html"
22
+
23
+ def __iter__(self):
24
+ for name, url in self.pages.items():
25
+ yield {"name": name, "url": url}
26
+
27
+ def __repr__(self):
28
+ return f"Category<{self.name}, {self.pages_count}>"
29
+
30
+ def __lt__(self, other):
31
+ return self.name < other.name
32
+
33
+
34
+ class Categories:
35
+
36
+ def __init__(self, basename):
37
+ self.basename = basename
38
+ self.categories = []
39
+
40
+ def get_or_create(self, name) -> Category:
41
+ for cat in self.categories:
42
+ if cat.name == name:
43
+ return cat
44
+ cat = Category(self, name)
45
+ self.categories.append(cat)
46
+ return cat
47
+
48
+ def __iter__(self):
49
+ for category in sorted(self.categories):
50
+ yield category
51
+
52
+ def __repr__(self):
53
+ return f"Categories<{self.categories}>"
54
+
@@ -0,0 +1,62 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ import click
5
+ from livereload import Server
6
+
7
+ from tashwiki import __version__
8
+ from tashwiki.config import Config
9
+ from tashwiki.builder import Builder
10
+
11
+ logger = logging.getLogger()
12
+ def_conf = Path("config.cfg")
13
+
14
+
15
+ @click.group("tashwiki")
16
+ @click.option(
17
+ "--config",
18
+ type=click.Path(),
19
+ help="Path to the configuration file."
20
+ )
21
+ @click.pass_context
22
+ def cli(ctx, config):
23
+ print(f"TashWiki v{__version__}")
24
+ if not config and def_conf.exists():
25
+ config = def_conf
26
+ if config:
27
+ conf = Config.from_file(config)
28
+ else:
29
+ conf = Config.default()
30
+ ctx.obj = {
31
+ "config": conf,
32
+ }
33
+
34
+
35
+ @cli.command()
36
+ @click.pass_context
37
+ def build(ctx):
38
+ """Build the website."""
39
+ config = ctx.obj["config"]
40
+ Builder(config).build()
41
+
42
+
43
+ @cli.command()
44
+ @click.option("--reload", is_flag=True, help="Enable automatic reloading.")
45
+ @click.option("--port", type=int, default=8080, help="Webserver port.")
46
+ @click.pass_context
47
+ def serve(ctx, reload, port):
48
+ """Run development web server"""
49
+ config = ctx.obj["config"]
50
+
51
+ def rebuild():
52
+ Builder(config).build()
53
+
54
+ server = Server()
55
+ if reload:
56
+ rebuild()
57
+ server.watch(config.site_source_dir, func=rebuild)
58
+ server.serve(port=port, root=config.site_output_dir)
59
+
60
+
61
+ def main():
62
+ cli()
@@ -0,0 +1,64 @@
1
+ import os
2
+ from configparser import ConfigParser, Error
3
+
4
+
5
+ class Config:
6
+
7
+ def __init__(self, config: ConfigParser):
8
+ self.conf = config
9
+
10
+ @classmethod
11
+ def from_file(cls, file: str):
12
+ if not os.path.exists(file):
13
+ raise ValueError("Config file not found.")
14
+ try:
15
+ config_parser = ConfigParser()
16
+ config_parser.read(file)
17
+ return cls(config_parser)
18
+ except Error:
19
+ raise ValueError("Config file is invalid.")
20
+
21
+ @classmethod
22
+ def default(cls):
23
+ config_parser = ConfigParser()
24
+ return cls(config_parser)
25
+
26
+ @property
27
+ def site_author(self):
28
+ return self.conf.get("site", "author", fallback="")
29
+
30
+ @property
31
+ def site_name(self):
32
+ return self.conf.get("site", "name", fallback="TashWiki")
33
+
34
+ @property
35
+ def site_language(self):
36
+ return self.conf.get("site", "language", fallback="en")
37
+
38
+ @property
39
+ def site_baseurl(self):
40
+ return self.conf.get("site", "baseurl", fallback="/")
41
+
42
+ @property
43
+ def site_source_dir(self):
44
+ return self.conf.get("site", "source_dir", fallback="content")
45
+
46
+ @property
47
+ def site_static_dir(self):
48
+ return self.conf.get("site", "static_dir", fallback="content/static")
49
+
50
+ @property
51
+ def site_output_dir(self):
52
+ return self.conf.get("site", "output_dir", fallback="output")
53
+
54
+ @property
55
+ def site_main_page(self):
56
+ return self.conf.get("site", "main_page", fallback="Main page")
57
+
58
+ @property
59
+ def site_categories_page(self):
60
+ return self.conf.get("site", "categories_page", fallback="Categories")
61
+
62
+ @property
63
+ def site_category_page(self):
64
+ return self.conf.get("site", "category_page", fallback="Category")
@@ -0,0 +1,84 @@
1
+ body {
2
+ font-family: sans-serif;
3
+ font-size: 16px;
4
+ }
5
+
6
+ main {
7
+ margin: 40px auto;
8
+ max-width: 750px;
9
+ line-height: 1.2;
10
+ color: #222;
11
+ padding: 0 10px;
12
+ hyphens: auto;
13
+ -moz-hyphens: auto;
14
+ -webkit-hyphens: auto;
15
+ -ms-hyphens: auto;
16
+ }
17
+
18
+ header {
19
+ display: flex;
20
+ flex-direction: row;
21
+ justify-content: flex-start;
22
+ align-items: center;
23
+ }
24
+
25
+ header .menu-item img {
26
+ margin: 0;
27
+ max-width: 100%;
28
+ }
29
+
30
+ header .menu-item {
31
+ margin-right: 10px;
32
+ }
33
+
34
+ pre code {
35
+ color: #111;
36
+ background: lightgray;
37
+ border: 1px solid #999;
38
+ border-radius: 4px;
39
+ display: block;
40
+ padding: 10px;
41
+ overflow: auto;
42
+ }
43
+
44
+ code {
45
+ color: #111;
46
+ background: lightgray;
47
+ border-style: solid;
48
+ border-width: 1px;
49
+ border: 1px solid #999;
50
+ border-radius: 4px;
51
+ padding: 1px;
52
+ }
53
+
54
+ img {
55
+ display: block;
56
+ margin: 0 auto;
57
+ max-width: 90%;
58
+ }
59
+
60
+ a {
61
+ color: #36c;
62
+ text-decoration: none;
63
+ }
64
+
65
+ a:visited {
66
+ color: #6a60b0;
67
+ }
68
+
69
+ a:hover {
70
+ text-decoration: underline;
71
+ }
72
+
73
+ a.notfound {
74
+ color: #bf3c2c;
75
+ }
76
+
77
+ footer {
78
+ clear: both;
79
+ padding-top: 1rem;
80
+ margin-top: 2rem;
81
+ text-align: center;
82
+ font-size: 0.8rem;
83
+ color: #333;
84
+ }
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html lang="{{ language }}">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="author" content="{{ site_author }}">
6
+ <meta name="generator" content="tashwiki">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <link rel="stylesheet" href="{{ baseurl }}css/style.css">
9
+ <link rel="icon" type="image/png" sizes="32x32" href="{{ baseurl }}icons/rocks-32px.png">
10
+ <link rel="icon" type="image/png" sizes="16x16" href="{{ baseurl }}icons/rocks-16px.png">
11
+ <title>{{ title }}</title>
12
+ </head>
13
+ <body>
14
+ <main>
15
+ <header>
16
+ <div class="menu-item"><img src="{{ baseurl }}icons/rocks-32px.png"></div>
17
+ <div class="menu-item"><a href="{{ baseurl }}">{{ main_page }}</a></div>
18
+ <div class="menu-item"><a href="{{ categories_url }}">{{ categories_page }}</a></div>
19
+ </header>
20
+ <hr>
21
+ <h1>{{ title }}</h1>
22
+ {% block content %}{% endblock %}
23
+ <footer>Generated by <a href="https://github.com/b00bl1k/tashwiki">TashWiki</a>.</footer>
24
+ </main>
25
+ </body>
26
+ </html>
@@ -0,0 +1,10 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <ul>
5
+ {% for category in categories %}
6
+ <li><a href="{{ baseurl }}{{ category.url }}">{{ category.name }}</a></li>
7
+ {% endfor %}
8
+ </ul>
9
+ <hr>
10
+ {% endblock %}
@@ -0,0 +1,10 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <ul>
5
+ {% for page in category %}
6
+ <li><a href="{{ baseurl }}{{ page.url }}">{{ page.name }}</a></li>
7
+ {% endfor %}
8
+ </ul>
9
+ <hr>
10
+ {% endblock %}
@@ -0,0 +1,13 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ {{ page_content }}
5
+ <hr>
6
+ {% if categories %}
7
+ <p>[ {{ categories_page }}:
8
+ {% for category in categories %}
9
+ <a href="{{ baseurl }}{{ category.url }}">{{ category.name }}</a></li>{% if not loop.last %}, {% endif %}
10
+ {% endfor %}
11
+ ]</p>
12
+ {% endif %}
13
+ {% endblock %}
@@ -0,0 +1,9 @@
1
+ import re
2
+
3
+
4
+ def page_name_to_label(page_name):
5
+ return re.sub(r"_+", " ", page_name)
6
+
7
+
8
+ def label_to_page_name(label):
9
+ return re.sub(r"([ ]+_)|(_[ ]+)|([ ]+)", "_", label)
@@ -0,0 +1,68 @@
1
+ """ Customised wikilinks extension
2
+
3
+ Based on markdown.extensions.wikilinks
4
+ """
5
+
6
+ import re
7
+ import xml.etree.ElementTree as etree
8
+ from typing import Any
9
+
10
+ from markdown.extensions import Extension
11
+ from markdown.inlinepatterns import InlineProcessor
12
+
13
+ from tashwiki.utils import label_to_page_name
14
+
15
+
16
+ def build_html_class(page):
17
+ return ""
18
+
19
+
20
+ class WikiLinkExtension(Extension):
21
+ """ Add inline processor to Markdown. """
22
+
23
+ def __init__(self, **kwargs):
24
+ """ Default configuration options. """
25
+ self.config = {
26
+ "base_url": ["/", "String to append to beginning or URL."],
27
+ "end_url": ["/", "String to append to end of URL."],
28
+ "html_class": [build_html_class, "Callback to return actual html class"],
29
+ }
30
+ super().__init__(**kwargs)
31
+
32
+ def extendMarkdown(self, md):
33
+ self.md = md
34
+
35
+ # append to end of inline patterns
36
+ WIKILINK_RE = r"\[\[([\(\)\w0-9_ -]+?)(?:\|([^\]]+?))?\]\]"
37
+ wikilinkPattern = WikiLinksInlineProcessor(WIKILINK_RE, self.getConfigs())
38
+ wikilinkPattern.md = md
39
+ md.inlinePatterns.register(wikilinkPattern, "wikilink", 75)
40
+
41
+
42
+ class WikiLinksInlineProcessor(InlineProcessor):
43
+ """ Build link from `wikilink`. """
44
+
45
+ def __init__(self, pattern: str, config: dict[str, Any]):
46
+ super().__init__(pattern)
47
+ self.config = config
48
+
49
+ def build_url(self, label: str) -> str:
50
+ return "{}{}{}".format(
51
+ self.config["base_url"],
52
+ label,
53
+ self.config["end_url"],
54
+ )
55
+
56
+ def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | str, int, int]:
57
+ if page := m.group(1).strip():
58
+ label = m.group(2).strip() if m.group(2) else page
59
+ url = self.build_url(label_to_page_name(page))
60
+ a = etree.Element("a")
61
+ a.text = label
62
+ a.set("href", url)
63
+ html_class = self.config["html_class"]
64
+ if html_class:
65
+ a.set("class", html_class(page))
66
+ else:
67
+ a = ""
68
+ return a, m.start(0), m.end(0)