From b2fdc788d4b64f229ec8873390bdb7c3aa17eaee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 4 May 2026 09:55:15 +0000 Subject: [PATCH] refactor: pass rendering context into preprocessor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Timothée Mazzucotelli --- crates/zensical/src/config/project.rs | 2 + python/zensical/compat/autorefs.py | 18 ++-- python/zensical/config.py | 4 +- python/zensical/extensions/context.py | 106 ++++++++++++++++++++++++ python/zensical/extensions/emoji.py | 6 +- python/zensical/extensions/glightbox.py | 21 ++--- python/zensical/extensions/links.py | 22 ++--- python/zensical/extensions/preview.py | 17 ++-- python/zensical/extensions/search.py | 20 +++-- python/zensical/markdown/extensions.py | 52 ------------ python/zensical/markdown/processors.py | 73 ---------------- python/zensical/markdown/render.py | 60 +++++++++----- 12 files changed, 205 insertions(+), 196 deletions(-) create mode 100644 python/zensical/extensions/context.py delete mode 100644 python/zensical/markdown/extensions.py delete mode 100644 python/zensical/markdown/processors.py diff --git a/crates/zensical/src/config/project.rs b/crates/zensical/src/config/project.rs index b1edd71..65c2ab6 100644 --- a/crates/zensical/src/config/project.rs +++ b/crates/zensical/src/config/project.rs @@ -46,6 +46,8 @@ use super::validation::Validation; #[derive(Debug, Hash, FromPyObject, Serialize)] #[pyo3(from_item_all)] pub struct Project { + /// Project root directory. + pub root_dir: PathBuf, /// Site name. pub site_name: String, /// Site URL. diff --git a/python/zensical/compat/autorefs.py b/python/zensical/compat/autorefs.py index 38effed..8321f73 100644 --- a/python/zensical/compat/autorefs.py +++ b/python/zensical/compat/autorefs.py @@ -30,6 +30,8 @@ if TYPE_CHECKING: AutorefsExtension, ) + from zensical.extensions.context import Page + # ---------------------------------------------------------------------------- # Global variables @@ -40,19 +42,11 @@ AUTOREFS: AutorefsPlugin | None = None # ---------------------------------------------------------------------------- # Classes # ---------------------------------------------------------------------------- -class AutorefsPage: - """Mock MkDocs pages.""" - - def __init__(self, url: str, path: str): - self.url = url - self.path = path - - class AutorefsPlugin: """Mock the autorefs plugin (data store).""" def __init__(self) -> None: - self.current_page: AutorefsPage | None = None + self.current_page: Page | None = None self.scan_toc: bool = True self.record_backlinks: bool = False @@ -63,7 +57,7 @@ class AutorefsPlugin: def register_anchor( self, - page: AutorefsPage, + page: Page, identifier: str, anchor: str | None = None, *, @@ -106,10 +100,10 @@ def get_autorefs_extension() -> AutorefsExtension | None: return AutorefsExtension(get_autorefs_plugin()) -def set_autorefs_page(url: str, path: str) -> None: +def set_autorefs_page(page: Page) -> None: """Set the current page for autorefs.""" plugin = get_autorefs_plugin() - plugin.current_page = AutorefsPage(url=url, path=path) + plugin.current_page = page def get_autorefs_data() -> dict[str, Any]: diff --git a/python/zensical/config.py b/python/zensical/config.py index 54b0d48..e9e9994 100644 --- a/python/zensical/config.py +++ b/python/zensical/config.py @@ -183,6 +183,8 @@ def _apply_defaults(config: dict, path: str) -> dict: We must set all properties, as well as nested properties to `None`, or PyO3 will refuse to convert them, as the key must definitely exist. """ + project_root = config["root_dir"] = os.path.dirname(path) + if "site_name" not in config: raise ConfigurationError("Missing required setting: site_name") @@ -197,7 +199,7 @@ def _apply_defaults(config: dict, path: str) -> dict: raise ConfigurationError("docs_dir must not contain '..'") # Validate that docs directory exists - docs_dir_path = os.path.join(os.path.dirname(path), config["docs_dir"]) + docs_dir_path = os.path.join(project_root, config["docs_dir"]) if not os.path.isdir(docs_dir_path): raise ConfigurationError( f"Docs directory does not exist: {docs_dir_path}" diff --git a/python/zensical/extensions/context.py b/python/zensical/extensions/context.py new file mode 100644 index 0000000..c7770d7 --- /dev/null +++ b/python/zensical/extensions/context.py @@ -0,0 +1,106 @@ +# Copyright (c) 2025-2026 Zensical and contributors + +# SPDX-License-Identifier: MIT +# All contributions are certified under the DCO + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from markdown import Extension +from markdown.preprocessors import Preprocessor + +if TYPE_CHECKING: + from markdown import Markdown + + +# ---------------------------------------------------------------------------- +# Classes +# ---------------------------------------------------------------------------- + + +class Page: + """A class representing a page being rendered.""" + + def __init__( + self, + url: str, + path: str, + title: str | None = None, + meta: dict | None = None, + ): + self.url = url + self.path = path + self.title: str | None = title + self.meta: dict = meta if meta is not None else {} + + +class ContextPreprocessor(Preprocessor): + """Preprocessor to store rendering context.""" + + name = "rendering_context" + + def __init__( + self, + md: Markdown, + page: Page, + config: dict[str, Any], + ): + super().__init__(md) + self.page = page + self.config = config + + def run(self, lines: list[str]) -> list[str]: + return lines + + @classmethod + def from_markdown(cls, md: Markdown) -> ContextPreprocessor | None: + """Lookup rendering context preprocessor from Markdown instance.""" + for processor in md.preprocessors: + if isinstance(processor, cls): + return processor + return None + + +class ContextExtension(Extension): + """Markdown extension to register rendering context.""" + + name = "zensical.extensions.context" + + def __init__(self, **kwargs: Any): + super().__init__() + self._kwargs = kwargs + + def extendMarkdown(self, md: Markdown) -> None: + """Register rendering context preprocessor.""" + # We must register the extension to ensure markdown-exec + # is able to forward it to its inner Markdown instances + md.registerExtension(self) + md.preprocessors.register( + ContextPreprocessor(md=md, **self._kwargs), + ContextPreprocessor.name, + 0, + ) + + +def makeExtension(**kwargs: Any) -> ContextExtension: + """Register Markdown extension.""" + return ContextExtension(**kwargs) diff --git a/python/zensical/extensions/emoji.py b/python/zensical/extensions/emoji.py index 6758907..5bbe4b5 100644 --- a/python/zensical/extensions/emoji.py +++ b/python/zensical/extensions/emoji.py @@ -32,14 +32,14 @@ from xml.etree.ElementTree import Element from pymdownx import emoji, twemoji_db if TYPE_CHECKING: - from zensical.markdown.extensions import MarkdownExt + from markdown import Markdown # ----------------------------------------------------------------------------- # Functions # ----------------------------------------------------------------------------- -def twemoji(options: dict, md: MarkdownExt) -> dict: # noqa: ARG001 +def twemoji(options: dict, md: Markdown) -> dict: # noqa: ARG001 """Create twemoji index.""" paths = options.get("custom_icons", [])[:] return _load_twemoji_index(tuple(paths)) @@ -54,7 +54,7 @@ def to_svg( title: str, category: str, options: dict, - md: MarkdownExt, + md: Markdown, ) -> Element[str]: """Load icon.""" if not uc: diff --git a/python/zensical/extensions/glightbox.py b/python/zensical/extensions/glightbox.py index f7a84dd..3b4f5f5 100644 --- a/python/zensical/extensions/glightbox.py +++ b/python/zensical/extensions/glightbox.py @@ -28,11 +28,12 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any from xml.etree.ElementTree import Element, ParseError, fromstring, tostring -from zensical.markdown.extensions import ExtensionExt, MarkdownExt -from zensical.markdown.processors import PostprocessorExt, TreeprocessorExt +from markdown import Extension, Markdown +from markdown.postprocessors import Postprocessor +from markdown.treeprocessors import Treeprocessor if TYPE_CHECKING: - from zensical.markdown.extensions import MarkdownExt + from markdown import Markdown # ----------------------------------------------------------------------------- # Constants @@ -62,14 +63,14 @@ class GlightboxConfig: # ----------------------------------------------------------------------------- -class GlightboxTreeprocessor(TreeprocessorExt): +class GlightboxTreeprocessor(Treeprocessor): """Wraps image elements in anchor tags to integrate with GLightbox.""" SKIP_CLASSES: frozenset[str] = frozenset( {"emojione", "twemoji", "gemoji", "off-glb"} ) - def __init__(self, md: MarkdownExt, config: GlightboxConfig): + def __init__(self, md: Markdown, config: GlightboxConfig): super().__init__(md) self.config = config @@ -190,7 +191,7 @@ class GlightboxTreeprocessor(TreeprocessorExt): ) -class GlightboxPostprocessor(PostprocessorExt): +class GlightboxPostprocessor(Postprocessor): """Wraps stashed images in anchors, delegating to the treeprocessor. This postprocessor uses a regular expression to find image tags in stashed @@ -199,8 +200,8 @@ class GlightboxPostprocessor(PostprocessorExt): parse and modify the HTML with an actual parser. """ - def __init__(self, md: MarkdownExt, processor: GlightboxTreeprocessor): - super().__init__(md) + def __init__(self, md: Markdown, processor: GlightboxTreeprocessor): + self.md: Markdown = md self._processor = processor self._processed: set[int] = set() @@ -244,7 +245,7 @@ class GlightboxPostprocessor(PostprocessorExt): # ----------------------------------------------------------------------------- -class GlightboxExtension(ExtensionExt): +class GlightboxExtension(Extension): """Markdown extension that wraps images in GLightbox anchor tags. This extension provides both a treeprocessor to wrap images in the normal @@ -281,7 +282,7 @@ class GlightboxExtension(ExtensionExt): } super().__init__(**kwargs) - def extendMarkdown(self, md: MarkdownExt) -> None: + def extendMarkdown(self, md: Markdown) -> None: """Register Markdown extension.""" md.registerExtension(self) config = GlightboxConfig(**self.getConfigs()) diff --git a/python/zensical/extensions/links.py b/python/zensical/extensions/links.py index 35cfb6f..547263d 100644 --- a/python/zensical/extensions/links.py +++ b/python/zensical/extensions/links.py @@ -28,15 +28,15 @@ from pathlib import PurePosixPath from typing import TYPE_CHECKING from urllib.parse import urlparse +from markdown import Extension +from markdown.postprocessors import Postprocessor +from markdown.treeprocessors import Treeprocessor from markdown.util import AMP_SUBSTITUTE -from zensical.markdown.extensions import ExtensionExt -from zensical.markdown.processors import PostprocessorExt, TreeprocessorExt - if TYPE_CHECKING: from xml.etree.ElementTree import Element - from zensical.markdown.extensions import MarkdownExt + from markdown import Markdown # ----------------------------------------------------------------------------- # Constants @@ -53,10 +53,10 @@ _RE = re.compile( # ----------------------------------------------------------------------------- -class LinksTreeprocessor(TreeprocessorExt): +class LinksTreeprocessor(Treeprocessor): """Rewrites relative links.""" - def __init__(self, md: MarkdownExt, path: str, use_directory_urls: bool): + def __init__(self, md: Markdown, path: str, use_directory_urls: bool): super().__init__(md) self.path = path self.use_directory_urls = use_directory_urls @@ -77,7 +77,7 @@ class LinksTreeprocessor(TreeprocessorExt): el.set(key, url) -class LinksPostprocessor(PostprocessorExt): +class LinksPostprocessor(Postprocessor): """Rewrites relative links in stashed raw HTML blocks. This postprocessor complements the :class:`LinksTreeprocessor` by applying @@ -86,8 +86,8 @@ class LinksPostprocessor(PostprocessorExt): inside raw HTML are handled consistently as well. """ - def __init__(self, md: MarkdownExt, path: str, use_directory_urls: bool): - super().__init__(md) + def __init__(self, md: Markdown, path: str, use_directory_urls: bool): + self.md: Markdown = md self._path = path self._use_directory_urls = use_directory_urls self._processed: set[int] = set() @@ -123,7 +123,7 @@ class LinksPostprocessor(PostprocessorExt): # ----------------------------------------------------------------------------- -class LinksExtension(ExtensionExt): +class LinksExtension(Extension): """Markdown extension to rewrite relative links to other files. Registers both a treeprocessor for links in the normal Markdown flow and @@ -137,7 +137,7 @@ class LinksExtension(ExtensionExt): self.path = path self.use_directory_urls = use_directory_urls - def extendMarkdown(self, md: MarkdownExt) -> None: + def extendMarkdown(self, md: Markdown) -> None: """Register Markdown extension.""" md.registerExtension(self) diff --git a/python/zensical/extensions/preview.py b/python/zensical/extensions/preview.py index 48942db..264138b 100644 --- a/python/zensical/extensions/preview.py +++ b/python/zensical/extensions/preview.py @@ -27,29 +27,32 @@ import posixpath from typing import TYPE_CHECKING, Any from urllib.parse import urlparse +from markdown import Extension +from markdown.treeprocessors import Treeprocessor + from zensical.extensions.links import LinksTreeprocessor from zensical.extensions.utilities.filter import Filter -from zensical.markdown.extensions import ExtensionExt, MarkdownExt -from zensical.markdown.processors import TreeprocessorExt if TYPE_CHECKING: from xml.etree.ElementTree import Element + from markdown import Markdown + # ----------------------------------------------------------------------------- # Classes # ----------------------------------------------------------------------------- -class PreviewProcessor(TreeprocessorExt): +class PreviewProcessor(Treeprocessor): """A Markdown treeprocessor to enable instant previews on links. Note that this treeprocessor is dependent on the `links` treeprocessor registered programmatically before rendering a page. """ - def __init__(self, md: MarkdownExt, config: dict): + def __init__(self, md: Markdown, config: dict): """Initialize the treeprocessor.""" - super().__init__(md) + self.md: Markdown = md self.config = config def run(self, root: Element) -> None: @@ -125,7 +128,7 @@ class PreviewProcessor(TreeprocessorExt): # ----------------------------------------------------------------------------- -class PreviewExtension(ExtensionExt): +class PreviewExtension(Extension): """Markdown extension to enable instant previews on links. This extensions allows to automatically add the `data-preview` attribute to @@ -143,7 +146,7 @@ class PreviewExtension(ExtensionExt): } super().__init__(*args, **kwargs) - def extendMarkdown(self, md: MarkdownExt) -> None: + def extendMarkdown(self, md: Markdown) -> None: """Register Markdown extension.""" md.registerExtension(self) diff --git a/python/zensical/extensions/search.py b/python/zensical/extensions/search.py index cf4a375..01405a8 100644 --- a/python/zensical/extensions/search.py +++ b/python/zensical/extensions/search.py @@ -21,22 +21,28 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. +from __future__ import annotations + from html import escape from html.parser import HTMLParser -from typing import Any +from typing import TYPE_CHECKING, Any + +from markdown import Extension +from markdown.postprocessors import Postprocessor + +if TYPE_CHECKING: + from markdown import Markdown -from zensical.markdown.extensions import ExtensionExt, MarkdownExt -from zensical.markdown.processors import PostprocessorExt # ----------------------------------------------------------------------------- # Classes # ----------------------------------------------------------------------------- -class SearchProcessor(PostprocessorExt): +class SearchProcessor(Postprocessor): """Post processor to extract searchable content from the rendered HTML.""" - def __init__(self, md: MarkdownExt) -> None: + def __init__(self, md: Markdown) -> None: super().__init__(md) self.data: list[dict[str, Any]] = [] @@ -71,14 +77,14 @@ class SearchProcessor(PostprocessorExt): return text -class SearchExtension(ExtensionExt): +class SearchExtension(Extension): """Markdown extension for search indexing.""" def __init__(self, **kwargs: Any) -> None: self.config = {"keep": [set(), "Set of HTML tags to keep in output"]} super().__init__(**kwargs) - def extendMarkdown(self, md: MarkdownExt) -> None: + def extendMarkdown(self, md: Markdown) -> None: """Register the PostProcessor with Markdown.""" processor = SearchProcessor(md) md.postprocessors.register(processor, "search", 0) diff --git a/python/zensical/markdown/extensions.py b/python/zensical/markdown/extensions.py deleted file mode 100644 index d16e9ae..0000000 --- a/python/zensical/markdown/extensions.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (c) 2025-2026 Zensical and contributors - -# SPDX-License-Identifier: MIT -# All contributions are certified under the DCO - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from __future__ import annotations - -from markdown import Extension, Markdown - -# ---------------------------------------------------------------------------- -# Classes -# ---------------------------------------------------------------------------- - - -class MarkdownExt(Markdown): - """Subclass of `Markdown`. - - We need to subclass the `Markdown` class to provide additional data to the - processors, such as page information and configuration, someting that isn't - supported by the original Markdown `Markdown` class. It allows to implement - several features that previously required MkDocs plugins more efficiently. - """ - - -class ExtensionExt(Extension): - """Subclass of `Extension`. - - We need to subclass the `Extension` to allow access to our modified - `MarkdownExt` instance, which includes the page and configuration. - """ - - def extendMarkdown(self, md: MarkdownExt) -> None: # ty:ignore[invalid-method-override] - """Register Markdown extension.""" - super().extendMarkdown(md) diff --git a/python/zensical/markdown/processors.py b/python/zensical/markdown/processors.py deleted file mode 100644 index 8917365..0000000 --- a/python/zensical/markdown/processors.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (c) 2025-2026 Zensical and contributors - -# SPDX-License-Identifier: MIT -# All contributions are certified under the DCO - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from markdown.postprocessors import Postprocessor -from markdown.preprocessors import Preprocessor -from markdown.treeprocessors import Treeprocessor - -if TYPE_CHECKING: - from zensical.markdown.extensions import MarkdownExt - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - - -class PreprocessorExt(Preprocessor): - """Subclass of `Preprocessor`. - - We need to subclass the `Preprocessor` to allow access to our modified - `MarkdownExt` instance, which includes the page and configuration. - """ - - def __init__(self, md: MarkdownExt): - """Initialize processor.""" - self.md: MarkdownExt = md - - -class PostprocessorExt(Postprocessor): - """Subclass of `Postprocessor`. - - We need to subclass the `Postprocessor` to allow access to our modified - `MarkdownExt` instance, which includes the page and configuration. - """ - - def __init__(self, md: MarkdownExt): - """Initialize processor.""" - self.md: MarkdownExt = md - - -class TreeprocessorExt(Treeprocessor): - """Subclass of `Treeprocessor`. - - We need to subclass the `Treeprocessor` to allow access to our modified - `MarkdownExt` instance, which includes the page and configuration. - """ - - def __init__(self, md: MarkdownExt): - """Initialize processor.""" - self.md: MarkdownExt = md diff --git a/python/zensical/markdown/render.py b/python/zensical/markdown/render.py index 25eac18..600c60a 100644 --- a/python/zensical/markdown/render.py +++ b/python/zensical/markdown/render.py @@ -28,13 +28,14 @@ from datetime import date, datetime from typing import TYPE_CHECKING, Any import yaml +from markdown import Markdown from yaml import SafeLoader from zensical.compat.autorefs import set_autorefs_page from zensical.config import get_config +from zensical.extensions.context import ContextExtension, Page from zensical.extensions.links import LinksExtension from zensical.extensions.search import SearchExtension -from zensical.markdown.extensions import MarkdownExt if TYPE_CHECKING: from zensical.extensions.search import SearchProcessor @@ -66,12 +67,43 @@ def render(content: str, path: str, url: str) -> dict: in order to support the specific syntax of Python Markdown. We're working on moving the entire rendering chain to Rust. """ - config = get_config() + # First, extract metadata - the Python Markdown parser brings a metadata + # extension, but the implementation is broken, as it does not support full + # YAML syntax, e.g. lists. Thus, we just parse the metadata with YAML. + meta: dict = {} + if match := FRONT_MATTER_RE.match(content): + try: + meta = yaml.load(match.group(1), SafeLoader) + if isinstance(meta, dict): + content = content[match.end() :].lstrip("\n") + else: + meta = {} + except Exception: # noqa: BLE001 + pass - set_autorefs_page(url, path) + # Create page context and set it for autorefs + page = Page(url=url, path=path, meta=meta) + set_autorefs_page(page) + + # Update configuration to include context extension + # It's important we mutate the global configuration here, + # to allow mkdocstrings to forward the extension + # to its inner Markdown instances + config = get_config() + for extension in config["markdown_extensions"]: + if isinstance(extension, ContextExtension): + extension._kwargs["page"] = page + break + else: + config["markdown_extensions"].append( + ContextExtension( + page=page, + config=config, + ) + ) # Initialize Markdown parser - md = MarkdownExt( + md = Markdown( extensions=config["markdown_extensions"], extension_configs=config["mdx_configs"], ) @@ -87,29 +119,17 @@ def render(content: str, path: str, url: str) -> dict: search_extension = SearchExtension() search_extension.extendMarkdown(md) - # First, extract metadata - the Python Markdown parser brings a metadata - # extension, but the implementation is broken, as it does not support full - # YAML syntax, e.g. lists. Thus, we just parse the metadata with YAML. - meta: dict = {} - if match := FRONT_MATTER_RE.match(content): - try: - meta = yaml.load(match.group(1), SafeLoader) - if isinstance(meta, dict): - content = content[match.end() :].lstrip("\n") - else: - meta = {} - except Exception: # noqa: BLE001 - pass - - # Convert Markdown and sanitize metadata before sending back to Rust + # Convert content to HTML content = md.convert(content) - meta = {k: _sanitize(v) for k, v in meta.items()} # Obtain search index data, unless page is excluded search_processor: SearchProcessor = md.postprocessors["search"] # ty:ignore[invalid-assignment] if meta.get("search", {}).get("exclude", False): search_processor.data = [] + # Sanitize metadata before passing it to Rust. + meta = {k: _sanitize(v) for k, v in meta.items()} + # Return Markdown with metadata return { "meta": meta,