zensical 0.0.3__cp310-abi3-musllinux_1_2_i686.whl → 0.0.12__cp310-abi3-musllinux_1_2_i686.whl
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.
Potentially problematic release.
This version of zensical might be problematic. Click here for more details.
- zensical/__init__.py +6 -6
- zensical/__main__.py +28 -0
- zensical/bootstrap/.github/workflows/docs.yml +10 -3
- zensical/bootstrap/zensical.toml +22 -22
- zensical/config.py +191 -197
- zensical/extensions/__init__.py +2 -2
- zensical/extensions/emoji.py +22 -27
- zensical/extensions/links.py +33 -24
- zensical/extensions/preview.py +29 -41
- zensical/extensions/search.py +83 -83
- zensical/extensions/utilities/__init__.py +2 -2
- zensical/extensions/utilities/filter.py +5 -10
- zensical/main.py +36 -47
- zensical/markdown.py +21 -20
- zensical/templates/assets/javascripts/bundle.21aa498e.min.js +3 -0
- zensical/templates/assets/javascripts/workers/{search.5e1f2129.min.js → search.5df7522c.min.js} +1 -1
- zensical/templates/assets/stylesheets/classic/main.6f483be1.min.css +1 -0
- zensical/templates/assets/stylesheets/modern/main.09f707be.min.css +1 -0
- zensical/templates/base.html +4 -4
- zensical/templates/partials/javascripts/base.html +1 -1
- zensical/templates/partials/nav-item.html +1 -1
- zensical/templates/partials/search.html +3 -1
- zensical/zensical.abi3.so +0 -0
- zensical/zensical.pyi +7 -13
- {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/METADATA +9 -4
- {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/RECORD +30 -29
- {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/WHEEL +1 -1
- {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/licenses/LICENSE.md +1 -1
- zensical.libs/libgcc_s-f5fcfe20.so.1 +0 -0
- zensical/templates/assets/javascripts/bundle.3c403d54.min.js +0 -3
- zensical/templates/assets/stylesheets/classic/main.c5ffb0a9.min.css +0 -1
- zensical/templates/assets/stylesheets/modern/main.1357c24d.min.css +0 -1
- zensical.libs/libgcc_s-27e5a392.so.1 +0 -0
- {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/entry_points.txt +0 -0
zensical/extensions/search.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Copyright (c) Zensical
|
|
1
|
+
# Copyright (c) 2025 Zensical and contributors
|
|
2
2
|
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
-
# Third-party contributions licensed under
|
|
4
|
+
# Third-party contributions licensed under DCO
|
|
5
5
|
|
|
6
6
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
7
|
# of this software and associated documentation files (the "Software"), to
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
|
|
24
24
|
from html import escape
|
|
25
25
|
from html.parser import HTMLParser
|
|
26
|
+
from typing import Any
|
|
26
27
|
|
|
27
|
-
from markdown import Extension
|
|
28
|
+
from markdown import Extension, Markdown
|
|
28
29
|
from markdown.postprocessors import Postprocessor
|
|
29
30
|
|
|
30
31
|
# -----------------------------------------------------------------------------
|
|
@@ -33,17 +34,14 @@ from markdown.postprocessors import Postprocessor
|
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class SearchProcessor(Postprocessor):
|
|
36
|
-
"""
|
|
37
|
-
Post processor that extracts searchable content from the rendered HTML.
|
|
38
|
-
"""
|
|
37
|
+
"""Post processor that extracts searchable content from the rendered HTML."""
|
|
39
38
|
|
|
40
|
-
def __init__(self, md):
|
|
39
|
+
def __init__(self, md: Markdown) -> None:
|
|
41
40
|
super().__init__(md)
|
|
42
|
-
self.data = []
|
|
41
|
+
self.data: list[dict[str, Any]] = []
|
|
43
42
|
|
|
44
|
-
def run(self, html):
|
|
43
|
+
def run(self, html: str) -> str:
|
|
45
44
|
"""Process the rendered HTML and extract text length."""
|
|
46
|
-
|
|
47
45
|
# Divide page content into sections
|
|
48
46
|
parser = Parser()
|
|
49
47
|
parser.feed(html)
|
|
@@ -76,17 +74,17 @@ class SearchProcessor(Postprocessor):
|
|
|
76
74
|
class SearchExtension(Extension):
|
|
77
75
|
"""Markdown extension for search indexing."""
|
|
78
76
|
|
|
79
|
-
def __init__(self, **kwargs):
|
|
77
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
80
78
|
self.config = {"keep": [set(), "Set of HTML tags to keep in output"]}
|
|
81
79
|
super().__init__(**kwargs)
|
|
82
80
|
|
|
83
|
-
def extendMarkdown(self, md):
|
|
81
|
+
def extendMarkdown(self, md: Markdown) -> None: # noqa: N802
|
|
84
82
|
"""Register the PostProcessor with Markdown."""
|
|
85
83
|
processor = SearchProcessor(md)
|
|
86
84
|
md.postprocessors.register(processor, "search", 0)
|
|
87
85
|
|
|
88
86
|
|
|
89
|
-
def makeExtension(**kwargs):
|
|
87
|
+
def makeExtension(**kwargs: Any) -> SearchExtension: # noqa: N802
|
|
90
88
|
"""Factory function for creating the extension."""
|
|
91
89
|
return SearchExtension(**kwargs)
|
|
92
90
|
|
|
@@ -96,13 +94,16 @@ def makeExtension(**kwargs):
|
|
|
96
94
|
|
|
97
95
|
# HTML element
|
|
98
96
|
class Element:
|
|
99
|
-
"""
|
|
97
|
+
"""HTML element.
|
|
98
|
+
|
|
100
99
|
An element with attributes, essentially a small wrapper object for the
|
|
101
100
|
parser to access attributes in other callbacks than handle_starttag.
|
|
102
101
|
"""
|
|
103
102
|
|
|
104
103
|
# Initialize HTML element
|
|
105
|
-
def __init__(
|
|
104
|
+
def __init__(
|
|
105
|
+
self, tag: str, attrs: dict[str, str | None] | None = None
|
|
106
|
+
) -> None:
|
|
106
107
|
self.tag = tag
|
|
107
108
|
self.attrs = attrs or {}
|
|
108
109
|
|
|
@@ -111,18 +112,17 @@ class Element:
|
|
|
111
112
|
return self.tag
|
|
112
113
|
|
|
113
114
|
# Support comparison (compare by tag only)
|
|
114
|
-
def __eq__(self, other):
|
|
115
|
-
if other
|
|
115
|
+
def __eq__(self, other: object) -> bool:
|
|
116
|
+
if isinstance(other, Element):
|
|
116
117
|
return self.tag == other.tag
|
|
117
|
-
|
|
118
|
-
return self.tag == other
|
|
118
|
+
return self.tag == other
|
|
119
119
|
|
|
120
120
|
# Support set operations
|
|
121
121
|
def __hash__(self):
|
|
122
122
|
return hash(self.tag)
|
|
123
123
|
|
|
124
124
|
# Check whether the element should be excluded
|
|
125
|
-
def is_excluded(self):
|
|
125
|
+
def is_excluded(self) -> bool:
|
|
126
126
|
return "data-search-exclude" in self.attrs
|
|
127
127
|
|
|
128
128
|
|
|
@@ -131,31 +131,31 @@ class Element:
|
|
|
131
131
|
|
|
132
132
|
# HTML section
|
|
133
133
|
class Section:
|
|
134
|
-
"""
|
|
134
|
+
"""HTML section.
|
|
135
|
+
|
|
135
136
|
A block of text with markup, preceded by a title (with markup), i.e., a
|
|
136
137
|
headline with a certain level (h1-h6). Internally used by the parser.
|
|
137
138
|
"""
|
|
138
139
|
|
|
139
140
|
# Initialize HTML section
|
|
140
|
-
def __init__(self, el, level, depth=0):
|
|
141
|
+
def __init__(self, el: Element, level: int, depth: int = 0) -> None:
|
|
141
142
|
self.el = el
|
|
142
|
-
self.depth = depth
|
|
143
|
+
self.depth: int | float = depth
|
|
143
144
|
self.level = level
|
|
144
145
|
|
|
145
146
|
# Initialize section data
|
|
146
|
-
self.text = []
|
|
147
|
-
self.title = []
|
|
148
|
-
self.id = None
|
|
147
|
+
self.text: list[str] = []
|
|
148
|
+
self.title: list[str] = []
|
|
149
|
+
self.id: str | None = None
|
|
149
150
|
|
|
150
151
|
# String representation
|
|
151
152
|
def __repr__(self):
|
|
152
153
|
if self.id:
|
|
153
|
-
return "
|
|
154
|
-
|
|
155
|
-
return self.el.tag
|
|
154
|
+
return f"{self.el.tag}#{self.id}"
|
|
155
|
+
return self.el.tag
|
|
156
156
|
|
|
157
157
|
# Check whether the section should be excluded
|
|
158
|
-
def is_excluded(self):
|
|
158
|
+
def is_excluded(self) -> bool:
|
|
159
159
|
return self.el.is_excluded()
|
|
160
160
|
|
|
161
161
|
|
|
@@ -164,7 +164,8 @@ class Section:
|
|
|
164
164
|
|
|
165
165
|
# HTML parser
|
|
166
166
|
class Parser(HTMLParser):
|
|
167
|
-
"""
|
|
167
|
+
"""Section divider.
|
|
168
|
+
|
|
168
169
|
This parser divides the given string of HTML into a list of sections, each
|
|
169
170
|
of which are preceded by a h1-h6 level heading. A white- and blacklist of
|
|
170
171
|
tags dictates which tags should be preserved as part of the index, and
|
|
@@ -172,31 +173,31 @@ class Parser(HTMLParser):
|
|
|
172
173
|
"""
|
|
173
174
|
|
|
174
175
|
# Initialize HTML parser
|
|
175
|
-
def __init__(self, *args, **kwargs):
|
|
176
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
176
177
|
super().__init__(*args, **kwargs)
|
|
177
178
|
|
|
178
179
|
# Tags to skip
|
|
179
|
-
self.skip =
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
]
|
|
185
|
-
)
|
|
180
|
+
self.skip: set[str | Element] = {
|
|
181
|
+
"object", # Objects
|
|
182
|
+
"script", # Scripts
|
|
183
|
+
"style", # Styles
|
|
184
|
+
}
|
|
186
185
|
|
|
187
186
|
# Current context and section
|
|
188
|
-
self.context = []
|
|
189
|
-
self.section = None
|
|
187
|
+
self.context: list[Element] = []
|
|
188
|
+
self.section: Section | None = None
|
|
190
189
|
|
|
191
190
|
# All parsed sections
|
|
192
|
-
self.data = []
|
|
191
|
+
self.data: list[Section] = []
|
|
193
192
|
|
|
194
193
|
# Called at the start of every HTML tag
|
|
195
|
-
def handle_starttag(
|
|
196
|
-
attrs
|
|
194
|
+
def handle_starttag(
|
|
195
|
+
self, tag: str, attrs: list[tuple[str, str | None]]
|
|
196
|
+
) -> None:
|
|
197
|
+
attrs_dict = dict(attrs)
|
|
197
198
|
|
|
198
199
|
# Ignore self-closing tags
|
|
199
|
-
el = Element(tag,
|
|
200
|
+
el = Element(tag, attrs_dict)
|
|
200
201
|
if tag not in void:
|
|
201
202
|
self.context.append(el)
|
|
202
203
|
else:
|
|
@@ -205,7 +206,7 @@ class Parser(HTMLParser):
|
|
|
205
206
|
# Handle heading
|
|
206
207
|
if tag in ([f"h{x}" for x in range(1, 7)]):
|
|
207
208
|
depth = len(self.context)
|
|
208
|
-
if "id" in
|
|
209
|
+
if "id" in attrs_dict:
|
|
209
210
|
# Ensure top-level section
|
|
210
211
|
if tag != "h1" and not self.data:
|
|
211
212
|
self.section = Section(Element("hx"), 1, depth)
|
|
@@ -214,7 +215,7 @@ class Parser(HTMLParser):
|
|
|
214
215
|
# Set identifier, if not first section
|
|
215
216
|
self.section = Section(el, int(tag[1:2]), depth)
|
|
216
217
|
if self.data:
|
|
217
|
-
self.section.id =
|
|
218
|
+
self.section.id = attrs_dict["id"]
|
|
218
219
|
|
|
219
220
|
# Append section to list
|
|
220
221
|
self.data.append(self.section)
|
|
@@ -225,7 +226,7 @@ class Parser(HTMLParser):
|
|
|
225
226
|
self.data.append(self.section)
|
|
226
227
|
|
|
227
228
|
# Handle special cases to skip
|
|
228
|
-
for key, value in
|
|
229
|
+
for key, value in attrs_dict.items():
|
|
229
230
|
# Skip block if explicitly excluded from search
|
|
230
231
|
if key == "data-search-exclude":
|
|
231
232
|
self.skip.add(el)
|
|
@@ -247,7 +248,7 @@ class Parser(HTMLParser):
|
|
|
247
248
|
data.append(f"<{tag}>")
|
|
248
249
|
|
|
249
250
|
# Called at the end of every HTML tag
|
|
250
|
-
def handle_endtag(self, tag):
|
|
251
|
+
def handle_endtag(self, tag: str) -> None:
|
|
251
252
|
if not self.context or self.context[-1] != tag:
|
|
252
253
|
return
|
|
253
254
|
|
|
@@ -255,6 +256,7 @@ class Parser(HTMLParser):
|
|
|
255
256
|
# a headline is nested in another element. In that case, we close the
|
|
256
257
|
# current section, continuing to append data to the previous section,
|
|
257
258
|
# which could also be a nested section – see https://bit.ly/3IxxIJZ
|
|
259
|
+
assert self.section is not None # noqa: S101
|
|
258
260
|
if self.section.depth > len(self.context):
|
|
259
261
|
for section in reversed(self.data):
|
|
260
262
|
if section.depth <= len(self.context):
|
|
@@ -295,7 +297,7 @@ class Parser(HTMLParser):
|
|
|
295
297
|
data.append(f"</{tag}>")
|
|
296
298
|
|
|
297
299
|
# Called for the text contents of each tag
|
|
298
|
-
def handle_data(self, data):
|
|
300
|
+
def handle_data(self, data: str) -> None:
|
|
299
301
|
if self.skip.intersection(self.context):
|
|
300
302
|
return
|
|
301
303
|
|
|
@@ -324,9 +326,11 @@ class Parser(HTMLParser):
|
|
|
324
326
|
|
|
325
327
|
# Collapse adjacent whitespace
|
|
326
328
|
elif data.isspace():
|
|
327
|
-
if
|
|
328
|
-
self.section.text
|
|
329
|
-
|
|
329
|
+
if (
|
|
330
|
+
not self.section.text
|
|
331
|
+
or not self.section.text[-1].isspace()
|
|
332
|
+
or "pre" in self.context
|
|
333
|
+
):
|
|
330
334
|
self.section.text.append(data)
|
|
331
335
|
|
|
332
336
|
# Handle everything else
|
|
@@ -339,35 +343,31 @@ class Parser(HTMLParser):
|
|
|
339
343
|
# -----------------------------------------------------------------------------
|
|
340
344
|
|
|
341
345
|
# Tags to keep
|
|
342
|
-
keep =
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
]
|
|
353
|
-
)
|
|
346
|
+
keep = {
|
|
347
|
+
"p",
|
|
348
|
+
"code",
|
|
349
|
+
"pre",
|
|
350
|
+
"li",
|
|
351
|
+
"ol",
|
|
352
|
+
"ul",
|
|
353
|
+
"sub",
|
|
354
|
+
"sup",
|
|
355
|
+
}
|
|
354
356
|
|
|
355
357
|
# Tags that are self-closing
|
|
356
|
-
void =
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
]
|
|
373
|
-
)
|
|
358
|
+
void = {
|
|
359
|
+
"area",
|
|
360
|
+
"base",
|
|
361
|
+
"br",
|
|
362
|
+
"col",
|
|
363
|
+
"embed",
|
|
364
|
+
"hr",
|
|
365
|
+
"img",
|
|
366
|
+
"input",
|
|
367
|
+
"link",
|
|
368
|
+
"meta",
|
|
369
|
+
"param",
|
|
370
|
+
"source",
|
|
371
|
+
"track",
|
|
372
|
+
"wbr",
|
|
373
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Copyright (c) Zensical
|
|
1
|
+
# Copyright (c) 2025 Zensical and contributors
|
|
2
2
|
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
-
# Third-party contributions licensed under
|
|
4
|
+
# Third-party contributions licensed under DCO
|
|
5
5
|
|
|
6
6
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
7
|
# of this software and associated documentation files (the "Software"), to
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Copyright (c) Zensical
|
|
1
|
+
# Copyright (c) 2025 Zensical and contributors
|
|
2
2
|
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
-
# Third-party contributions licensed under
|
|
4
|
+
# Third-party contributions licensed under DCO
|
|
5
5
|
|
|
6
6
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
7
|
# of this software and associated documentation files (the "Software"), to
|
|
@@ -31,13 +31,10 @@ from fnmatch import fnmatch
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class Filter:
|
|
34
|
-
"""
|
|
35
|
-
A filter.
|
|
36
|
-
"""
|
|
34
|
+
"""A filter."""
|
|
37
35
|
|
|
38
36
|
def __init__(self, config: dict):
|
|
39
|
-
"""
|
|
40
|
-
Initialize the filter.
|
|
37
|
+
"""Initialize the filter.
|
|
41
38
|
|
|
42
39
|
Arguments:
|
|
43
40
|
config: The filter configuration.
|
|
@@ -45,8 +42,7 @@ class Filter:
|
|
|
45
42
|
self.config = config
|
|
46
43
|
|
|
47
44
|
def __call__(self, value: str) -> bool:
|
|
48
|
-
"""
|
|
49
|
-
Filter a value.
|
|
45
|
+
"""Filter a value.
|
|
50
46
|
|
|
51
47
|
First, the inclusion patterns are checked. Regardless of whether they
|
|
52
48
|
are present, the exclusion patterns are checked afterwards. This allows
|
|
@@ -59,7 +55,6 @@ class Filter:
|
|
|
59
55
|
Returns:
|
|
60
56
|
Whether the value should be included.
|
|
61
57
|
"""
|
|
62
|
-
|
|
63
58
|
# Check if value matches one of the inclusion patterns
|
|
64
59
|
if "include" in self.config:
|
|
65
60
|
for pattern in self.config["include"]:
|
zensical/main.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Copyright (c) Zensical
|
|
1
|
+
# Copyright (c) 2025 Zensical and contributors
|
|
2
2
|
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
-
# Third-party contributions licensed under
|
|
4
|
+
# Third-party contributions licensed under DCO
|
|
5
5
|
|
|
6
6
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
7
|
# of this software and associated documentation files (the "Software"), to
|
|
@@ -23,12 +23,14 @@
|
|
|
23
23
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
|
-
import click
|
|
27
26
|
import os
|
|
28
27
|
import shutil
|
|
29
|
-
import
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
30
|
|
|
31
|
+
import click
|
|
31
32
|
from click import ClickException
|
|
33
|
+
|
|
32
34
|
from zensical import build, serve, version
|
|
33
35
|
|
|
34
36
|
# ----------------------------------------------------------------------------
|
|
@@ -38,8 +40,8 @@ from zensical import build, serve, version
|
|
|
38
40
|
|
|
39
41
|
@click.version_option(version=version(), message="%(version)s")
|
|
40
42
|
@click.group()
|
|
41
|
-
def cli():
|
|
42
|
-
"""Zensical - A modern static site generator"""
|
|
43
|
+
def cli() -> None:
|
|
44
|
+
"""Zensical - A modern static site generator."""
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
@cli.command(name="build")
|
|
@@ -64,10 +66,8 @@ def cli():
|
|
|
64
66
|
is_flag=True,
|
|
65
67
|
help="Strict mode (currently unsupported).",
|
|
66
68
|
)
|
|
67
|
-
def execute_build(config_file: str | None, **kwargs):
|
|
68
|
-
"""
|
|
69
|
-
Build a project.
|
|
70
|
-
"""
|
|
69
|
+
def execute_build(config_file: str | None, **kwargs: Any) -> None:
|
|
70
|
+
"""Build a project."""
|
|
71
71
|
if config_file is None:
|
|
72
72
|
for file in ["zensical.toml", "mkdocs.yml", "mkdocs.yaml"]:
|
|
73
73
|
if os.path.exists(file):
|
|
@@ -80,7 +80,7 @@ def execute_build(config_file: str | None, **kwargs):
|
|
|
80
80
|
|
|
81
81
|
# Build project in Rust runtime, calling back into Python when necessary,
|
|
82
82
|
# e.g., to parse MkDocs configuration format or render Markdown
|
|
83
|
-
build(os.path.abspath(config_file), kwargs.get("clean"))
|
|
83
|
+
build(os.path.abspath(config_file), kwargs.get("clean", False))
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
@cli.command(name="serve")
|
|
@@ -111,10 +111,8 @@ def execute_build(config_file: str | None, **kwargs):
|
|
|
111
111
|
is_flag=True,
|
|
112
112
|
help="Strict mode (currently unsupported).",
|
|
113
113
|
)
|
|
114
|
-
def execute_serve(config_file: str | None, **kwargs):
|
|
115
|
-
"""
|
|
116
|
-
Build and serve a project.
|
|
117
|
-
"""
|
|
114
|
+
def execute_serve(config_file: str | None, **kwargs: Any) -> None:
|
|
115
|
+
"""Build and serve a project."""
|
|
118
116
|
if config_file is None:
|
|
119
117
|
for file in ["zensical.toml", "mkdocs.yml", "mkdocs.yaml"]:
|
|
120
118
|
if os.path.exists(file):
|
|
@@ -122,17 +120,12 @@ def execute_serve(config_file: str | None, **kwargs):
|
|
|
122
120
|
break
|
|
123
121
|
else:
|
|
124
122
|
raise ClickException("No config file found in the current folder.")
|
|
125
|
-
|
|
126
|
-
# Obtain development server address and open in browser, if desired
|
|
127
|
-
dev_addr = kwargs.get("dev_addr") or "localhost:8000"
|
|
128
|
-
if kwargs.get("open", False):
|
|
129
|
-
webbrowser.open(f"http://{dev_addr}")
|
|
130
123
|
if kwargs.get("strict", False):
|
|
131
124
|
print("Warning: Strict mode is currently unsupported.")
|
|
132
125
|
|
|
133
126
|
# Build project in Rust runtime, calling back into Python when necessary,
|
|
134
127
|
# e.g., to parse MkDocs configuration format or render Markdown
|
|
135
|
-
serve(os.path.abspath(config_file),
|
|
128
|
+
serve(os.path.abspath(config_file), kwargs)
|
|
136
129
|
|
|
137
130
|
|
|
138
131
|
@cli.command(name="new")
|
|
@@ -141,38 +134,34 @@ def execute_serve(config_file: str | None, **kwargs):
|
|
|
141
134
|
type=click.Path(file_okay=False, dir_okay=True, writable=True),
|
|
142
135
|
required=False,
|
|
143
136
|
)
|
|
144
|
-
def new_project(directory: str | None, **kwargs):
|
|
145
|
-
"""
|
|
146
|
-
Create a new template project in the current directory or in the given
|
|
147
|
-
directory.
|
|
137
|
+
def new_project(directory: str | None, **kwargs: Any) -> None: # noqa: ARG001
|
|
138
|
+
"""Create a new template project in the current directory or in the given directory.
|
|
148
139
|
|
|
149
140
|
Raises:
|
|
150
141
|
ClickException: if the directory already contains a zensical.toml or a
|
|
151
142
|
docs directory that is not empty, as well as when the path provided
|
|
152
143
|
points to something that is not a directory.
|
|
153
144
|
"""
|
|
154
|
-
|
|
155
|
-
if
|
|
156
|
-
directory
|
|
157
|
-
|
|
158
|
-
config_file =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
os.path.join(directory, "docs"),
|
|
175
|
-
)
|
|
145
|
+
working_dir = Path.cwd() if directory is None else Path(directory).resolve()
|
|
146
|
+
if working_dir.is_file():
|
|
147
|
+
raise ClickException(f"{working_dir} must be a directory, not a file.")
|
|
148
|
+
|
|
149
|
+
config_file = working_dir / "zensical.toml"
|
|
150
|
+
if config_file.exists():
|
|
151
|
+
raise ClickException(f"{config_file} already exists.")
|
|
152
|
+
|
|
153
|
+
working_dir.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
|
|
155
|
+
package_dir = Path(__file__).resolve().parent
|
|
156
|
+
bootstrap = package_dir / "bootstrap"
|
|
157
|
+
|
|
158
|
+
for src_file in bootstrap.rglob("*"):
|
|
159
|
+
if src_file.is_file():
|
|
160
|
+
rel_path = src_file.relative_to(bootstrap)
|
|
161
|
+
dest_file = working_dir / rel_path
|
|
162
|
+
if not dest_file.exists():
|
|
163
|
+
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
shutil.copyfile(src_file, dest_file)
|
|
176
165
|
|
|
177
166
|
|
|
178
167
|
# ----------------------------------------------------------------------------
|
zensical/markdown.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Copyright (c) Zensical
|
|
1
|
+
# Copyright (c) 2025 Zensical and contributors
|
|
2
2
|
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
-
# Third-party contributions licensed under
|
|
4
|
+
# Third-party contributions licensed under DCO
|
|
5
5
|
|
|
6
6
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
7
|
# of this software and associated documentation files (the "Software"), to
|
|
@@ -24,15 +24,19 @@
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
26
|
import re
|
|
27
|
-
import yaml
|
|
28
|
-
|
|
29
27
|
from datetime import date, datetime
|
|
28
|
+
from typing import TYPE_CHECKING, Any
|
|
29
|
+
|
|
30
|
+
import yaml
|
|
30
31
|
from markdown import Markdown
|
|
31
32
|
from yaml import SafeLoader
|
|
32
33
|
|
|
33
|
-
from .config import
|
|
34
|
-
from .extensions.links import LinksExtension
|
|
35
|
-
from .extensions.search import SearchExtension
|
|
34
|
+
from zensical.config import get_config
|
|
35
|
+
from zensical.extensions.links import LinksExtension
|
|
36
|
+
from zensical.extensions.search import SearchExtension
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from zensical.extensions.search import SearchProcessor
|
|
36
40
|
|
|
37
41
|
# ----------------------------------------------------------------------------
|
|
38
42
|
# Constants
|
|
@@ -53,15 +57,14 @@ Regex pattern to extract front matter.
|
|
|
53
57
|
|
|
54
58
|
|
|
55
59
|
def render(content: str, path: str) -> dict:
|
|
56
|
-
"""
|
|
57
|
-
Render Markdown and return HTML.
|
|
60
|
+
"""Render Markdown and return HTML.
|
|
58
61
|
|
|
59
62
|
This function returns rendered HTML as well as the table of contents and
|
|
60
63
|
metadata. Now, this is the part where Zensical needs to call into Python,
|
|
61
64
|
in order to support the specific syntax of Python Markdown. We're working
|
|
62
65
|
on moving the entire rendering chain to Rust.
|
|
63
66
|
"""
|
|
64
|
-
config =
|
|
67
|
+
config = get_config()
|
|
65
68
|
|
|
66
69
|
# Initialize Markdown parser
|
|
67
70
|
md = Markdown(
|
|
@@ -77,8 +80,8 @@ def render(content: str, path: str) -> dict:
|
|
|
77
80
|
links.extendMarkdown(md)
|
|
78
81
|
|
|
79
82
|
# Register search extension, which extracts text for search indexing
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
search_extension = SearchExtension()
|
|
84
|
+
search_extension.extendMarkdown(md)
|
|
82
85
|
|
|
83
86
|
# First, extract metadata - the Python Markdown parser brings a metadata
|
|
84
87
|
# extension, but the implementation is broken, as it does not support full
|
|
@@ -91,7 +94,7 @@ def render(content: str, path: str) -> dict:
|
|
|
91
94
|
content = content[match.end() :].lstrip("\n")
|
|
92
95
|
else:
|
|
93
96
|
meta = {}
|
|
94
|
-
except Exception:
|
|
97
|
+
except Exception: # noqa: BLE001
|
|
95
98
|
pass
|
|
96
99
|
|
|
97
100
|
# Convert Markdown and set nullish metadata to empty string, since we
|
|
@@ -106,24 +109,22 @@ def render(content: str, path: str) -> dict:
|
|
|
106
109
|
meta[key] = value.isoformat()
|
|
107
110
|
|
|
108
111
|
# Obtain search index data, unless page is excluded
|
|
109
|
-
|
|
112
|
+
search_processor: SearchProcessor = md.postprocessors["search"] # type: ignore[assignment]
|
|
110
113
|
if meta.get("search", {}).get("exclude", False):
|
|
111
|
-
|
|
114
|
+
search_processor.data = []
|
|
112
115
|
|
|
113
116
|
# Return Markdown with metadata
|
|
114
117
|
return {
|
|
115
118
|
"meta": meta,
|
|
116
119
|
"content": content,
|
|
117
|
-
"search":
|
|
120
|
+
"search": search_processor.data,
|
|
118
121
|
"title": "",
|
|
119
122
|
"toc": [_convert_toc(item) for item in getattr(md, "toc_tokens", [])],
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
|
|
123
|
-
def _convert_toc(item:
|
|
124
|
-
"""
|
|
125
|
-
Convert a table of contents item to navigation item format.
|
|
126
|
-
"""
|
|
126
|
+
def _convert_toc(item: Any) -> dict:
|
|
127
|
+
"""Convert a table of contents item to navigation item format."""
|
|
127
128
|
toc_item = {
|
|
128
129
|
"title": item["data-toc-label"] or item["name"],
|
|
129
130
|
"id": item["id"],
|