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.

Files changed (34) hide show
  1. zensical/__init__.py +6 -6
  2. zensical/__main__.py +28 -0
  3. zensical/bootstrap/.github/workflows/docs.yml +10 -3
  4. zensical/bootstrap/zensical.toml +22 -22
  5. zensical/config.py +191 -197
  6. zensical/extensions/__init__.py +2 -2
  7. zensical/extensions/emoji.py +22 -27
  8. zensical/extensions/links.py +33 -24
  9. zensical/extensions/preview.py +29 -41
  10. zensical/extensions/search.py +83 -83
  11. zensical/extensions/utilities/__init__.py +2 -2
  12. zensical/extensions/utilities/filter.py +5 -10
  13. zensical/main.py +36 -47
  14. zensical/markdown.py +21 -20
  15. zensical/templates/assets/javascripts/bundle.21aa498e.min.js +3 -0
  16. zensical/templates/assets/javascripts/workers/{search.5e1f2129.min.js → search.5df7522c.min.js} +1 -1
  17. zensical/templates/assets/stylesheets/classic/main.6f483be1.min.css +1 -0
  18. zensical/templates/assets/stylesheets/modern/main.09f707be.min.css +1 -0
  19. zensical/templates/base.html +4 -4
  20. zensical/templates/partials/javascripts/base.html +1 -1
  21. zensical/templates/partials/nav-item.html +1 -1
  22. zensical/templates/partials/search.html +3 -1
  23. zensical/zensical.abi3.so +0 -0
  24. zensical/zensical.pyi +7 -13
  25. {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/METADATA +9 -4
  26. {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/RECORD +30 -29
  27. {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/WHEEL +1 -1
  28. {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/licenses/LICENSE.md +1 -1
  29. zensical.libs/libgcc_s-f5fcfe20.so.1 +0 -0
  30. zensical/templates/assets/javascripts/bundle.3c403d54.min.js +0 -3
  31. zensical/templates/assets/stylesheets/classic/main.c5ffb0a9.min.css +0 -1
  32. zensical/templates/assets/stylesheets/modern/main.1357c24d.min.css +0 -1
  33. zensical.libs/libgcc_s-27e5a392.so.1 +0 -0
  34. {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
1
- # Copyright (c) Zensical LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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__(self, tag, attrs=None):
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 is Element:
115
+ def __eq__(self, other: object) -> bool:
116
+ if isinstance(other, Element):
116
117
  return self.tag == other.tag
117
- else:
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 "#".join([self.el.tag, self.id])
154
- else:
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 = set(
180
- [
181
- "object", # Objects
182
- "script", # Scripts
183
- "style", # Styles
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(self, tag, attrs):
196
- attrs = dict(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, attrs)
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 attrs:
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 = attrs["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 attrs.items():
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 not self.section.text or not self.section.text[-1].isspace():
328
- self.section.text.append(data)
329
- elif "pre" in self.context:
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 = set(
343
- [
344
- "p",
345
- "code",
346
- "pre",
347
- "li",
348
- "ol",
349
- "ul",
350
- "sub",
351
- "sup",
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 = set(
357
- [
358
- "area",
359
- "base",
360
- "br",
361
- "col",
362
- "embed",
363
- "hr",
364
- "img",
365
- "input",
366
- "link",
367
- "meta",
368
- "param",
369
- "source",
370
- "track",
371
- "wbr",
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 LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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 LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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 LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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 webbrowser
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), dev_addr)
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 directory is None:
156
- directory = "."
157
- docs_dir = os.path.join(directory, "docs")
158
- config_file = os.path.join(directory, "zensical.toml")
159
-
160
- if os.path.exists(directory):
161
- if not os.path.isdir(directory):
162
- raise (ClickException("Path provided is not a directory."))
163
- if os.path.exists(config_file):
164
- raise (ClickException(f"{config_file} already exists."))
165
- if os.path.exists(docs_dir):
166
- raise (ClickException(f"{docs_dir} already exists."))
167
- else:
168
- os.makedirs(directory)
169
-
170
- package_dir = os.path.dirname(os.path.abspath(__file__))
171
- shutil.copy(os.path.join(package_dir, "bootstrap/zensical.toml"), directory)
172
- shutil.copytree(
173
- os.path.join(package_dir, "bootstrap/docs"),
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 LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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 _CONFIG
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 = _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
- search = SearchExtension()
81
- search.extendMarkdown(md)
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
- search = md.postprocessors["search"]
112
+ search_processor: SearchProcessor = md.postprocessors["search"] # type: ignore[assignment]
110
113
  if meta.get("search", {}).get("exclude", False):
111
- search.data = []
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": search.data,
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: any):
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"],