mirror of
https://github.com/zensical/zensical.git
synced 2026-08-23 15:46:44 +00:00
refactor: pass rendering context into preprocessor
Signed-off-by: Timothée Mazzucotelli <dev@pawamoy.fr>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user