mirror of
https://github.com/zensical/zensical.git
synced 2026-05-03 17:40:31 +00:00
Fix glightbox captions (#605)
Signed-off-by: squidfunk <martin.donath@squidfunk.com>
This commit is contained in:
+1
-1
@@ -56,6 +56,7 @@ ignore = [
|
||||
"ERA001", # Commented out code
|
||||
"FBT001", # Boolean positional parameter in function signature
|
||||
"FBT003", # Boolean positional value in function call
|
||||
"N802", # Function name should be lowercase
|
||||
"G004", # Logging statement uses f-string
|
||||
"PLR0911", # Too many return statements
|
||||
"PLR0912", # Too many branches
|
||||
@@ -100,4 +101,3 @@ convention = "google"
|
||||
[format]
|
||||
docstring-code-format = true
|
||||
docstring-code-line-length = 80
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
//! Markdown rendering.
|
||||
|
||||
use anyhow::Result;
|
||||
use pyo3::types::PyAnyMethods;
|
||||
use pyo3::types::{PyAnyMethods, PyTracebackMethods};
|
||||
use pyo3::{FromPyObject, Python};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
@@ -85,12 +85,21 @@ impl Markdown {
|
||||
module
|
||||
.call_method1("render", (content, id.location(), url))?
|
||||
.extract::<Markdown>()
|
||||
})
|
||||
.map_err(|err| {
|
||||
Python::attach(|py| {
|
||||
let traceback = err
|
||||
.traceback(py)
|
||||
.and_then(|tb| tb.format().ok())
|
||||
.unwrap_or_default();
|
||||
anyhow::anyhow!("Python error: {err}\n{traceback}")
|
||||
})
|
||||
});
|
||||
|
||||
// Explicitly drop the lock guard here, so we're sure to hold it just
|
||||
// until after Python finished executing the rendering logic
|
||||
drop(guard);
|
||||
res.map_err(Into::into).map(|markdown| Markdown {
|
||||
res.map(|markdown| Markdown {
|
||||
title: extract_title(&id, &markdown),
|
||||
meta: markdown.meta,
|
||||
content: markdown.content,
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
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
|
||||
@@ -45,6 +46,22 @@ _RE = re.compile(r"<img\s[^>]*?>", re.IGNORECASE)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class GlightboxConfig:
|
||||
"""Configuration for the Glightbox Markdown extension."""
|
||||
|
||||
width: str = "auto"
|
||||
height: str = "auto"
|
||||
skip_classes: list[str] = field(default_factory=list)
|
||||
auto: bool = True
|
||||
auto_themed: bool = False
|
||||
auto_caption: bool = False
|
||||
caption_position: str = "bottom"
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class GlightboxTreeprocessor(TreeprocessorExt):
|
||||
"""Wraps image elements in anchor tags to integrate with GLightbox."""
|
||||
|
||||
@@ -52,15 +69,13 @@ class GlightboxTreeprocessor(TreeprocessorExt):
|
||||
{"emojione", "twemoji", "gemoji", "off-glb"}
|
||||
)
|
||||
|
||||
def __init__(self, md: MarkdownExt, config: dict[str, object]):
|
||||
def __init__(self, md: MarkdownExt, config: GlightboxConfig):
|
||||
super().__init__(md)
|
||||
self.config = config
|
||||
|
||||
def run(self, root: Element) -> None:
|
||||
"""Walk the element tree and wrap images with anchors."""
|
||||
skip_classes = self.SKIP_CLASSES | frozenset(
|
||||
cast("list[str]", self.config.get("skip_classes") or [])
|
||||
)
|
||||
skip_classes = self.SKIP_CLASSES | frozenset(self.config.skip_classes)
|
||||
|
||||
# Iterate over all images in the tree and wrap them with anchors
|
||||
for img in list(root.iter("img")):
|
||||
@@ -74,7 +89,7 @@ class GlightboxTreeprocessor(TreeprocessorExt):
|
||||
return True
|
||||
|
||||
# If manual mode is enabled, only wrap images explicitly marked
|
||||
return not self.config.get("auto") and "on-glb" not in classes
|
||||
return not self.config.auto and "on-glb" not in classes
|
||||
|
||||
def _wrap_with_anchor(self, img: Element, root: Element) -> None:
|
||||
"""Wrap an image with an anchor."""
|
||||
@@ -106,16 +121,14 @@ class GlightboxTreeprocessor(TreeprocessorExt):
|
||||
el.set("data-type", "image")
|
||||
|
||||
# Only set width/height if explicitly configured
|
||||
if width := self.config.get("width"):
|
||||
el.set("data-width", str(width))
|
||||
if height := self.config.get("height"):
|
||||
el.set("data-height", str(height))
|
||||
if self.config.width != "auto":
|
||||
el.set("data-width", self.config.width)
|
||||
if self.config.height != "auto":
|
||||
el.set("data-height", self.config.height)
|
||||
|
||||
# Set image title
|
||||
auto_caption = bool(self.config.get("auto_caption", False))
|
||||
title = img.get("data-title") or (
|
||||
img.get("alt") if auto_caption else None
|
||||
)
|
||||
# Set image title, or auto-caption from alt if enabled
|
||||
alt = img.get("alt") if self.config.auto_caption else None
|
||||
title = img.get("data-title", alt)
|
||||
if title:
|
||||
el.set("data-title", title)
|
||||
|
||||
@@ -124,11 +137,11 @@ class GlightboxTreeprocessor(TreeprocessorExt):
|
||||
el.set("data-description", description)
|
||||
|
||||
# Set image description position
|
||||
if caption_position := (
|
||||
img.get("data-caption-position")
|
||||
or self.config.get("caption_position")
|
||||
):
|
||||
el.set("data-desc-position", str(caption_position))
|
||||
caption_position = img.get(
|
||||
"data-caption-position", self.config.caption_position
|
||||
)
|
||||
if caption_position and caption_position != "bottom":
|
||||
el.set("data-desc-position", caption_position)
|
||||
|
||||
# Set gallery grouping
|
||||
if gallery := self._resolve_gallery(img):
|
||||
@@ -156,7 +169,7 @@ class GlightboxTreeprocessor(TreeprocessorExt):
|
||||
|
||||
# If auto-themed grouping is enabled, group images by light/dark mode
|
||||
# hints in the URL (e.g. from GitHub's light/dark mode image syntax)
|
||||
if self.config.get("auto_themed"):
|
||||
if self.config.auto_themed:
|
||||
if "#only-light" in src or "#gh-light-mode-only" in src:
|
||||
return "light"
|
||||
if "#only-dark" in src or "#gh-dark-mode-only" in src:
|
||||
@@ -182,14 +195,14 @@ class GlightboxPostprocessor(PostprocessorExt):
|
||||
parse and modify the HTML with an actual parser.
|
||||
"""
|
||||
|
||||
def __init__(self, md: MarkdownExt, config: dict[str, object]):
|
||||
def __init__(self, md: MarkdownExt, processor: GlightboxTreeprocessor):
|
||||
super().__init__(md)
|
||||
self._processor = GlightboxTreeprocessor(md, config)
|
||||
self._processor = processor
|
||||
self._processed: set[int] = set()
|
||||
|
||||
# Source classes to skip from postprocessor
|
||||
self._skip_classes = GlightboxTreeprocessor.SKIP_CLASSES | frozenset(
|
||||
cast("list[str]", config.get("skip_classes") or [])
|
||||
processor.config.skip_classes
|
||||
)
|
||||
|
||||
def run(self, text: str) -> str:
|
||||
@@ -258,22 +271,23 @@ class GlightboxExtension(ExtensionExt):
|
||||
"Use img alt attribute as the caption when no title is set.",
|
||||
],
|
||||
"caption_position": [
|
||||
None,
|
||||
"bottom",
|
||||
"Default caption position: bottom, top, left, or right.",
|
||||
],
|
||||
}
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def extendMarkdown(self, md: MarkdownExt) -> None: # noqa: N802
|
||||
def extendMarkdown(self, md: MarkdownExt) -> None:
|
||||
"""Register Markdown extension."""
|
||||
md.registerExtension(self)
|
||||
config = GlightboxConfig(**self.getConfigs())
|
||||
|
||||
# Register treeprocessor - run after `attr_list` (priority 8)
|
||||
treeprocessor = GlightboxTreeprocessor(md, self.getConfigs())
|
||||
treeprocessor = GlightboxTreeprocessor(md, config)
|
||||
md.treeprocessors.register(treeprocessor, "glightbox", 7)
|
||||
|
||||
# Register postprocessor - run before `raw_html` (priority 30)
|
||||
postprocessor = GlightboxPostprocessor(md, self.getConfigs())
|
||||
postprocessor = GlightboxPostprocessor(md, treeprocessor)
|
||||
md.postprocessors.register(postprocessor, "glightbox", 31)
|
||||
|
||||
|
||||
@@ -282,6 +296,6 @@ class GlightboxExtension(ExtensionExt):
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def makeExtension(**kwargs: Any) -> GlightboxExtension: # noqa: N802
|
||||
def makeExtension(**kwargs: Any) -> GlightboxExtension:
|
||||
"""Register Markdown extension."""
|
||||
return GlightboxExtension(**kwargs)
|
||||
|
||||
@@ -137,7 +137,7 @@ class LinksExtension(ExtensionExt):
|
||||
self.path = path
|
||||
self.use_directory_urls = use_directory_urls
|
||||
|
||||
def extendMarkdown(self, md: MarkdownExt) -> None: # noqa: N802
|
||||
def extendMarkdown(self, md: MarkdownExt) -> None:
|
||||
"""Register Markdown extension."""
|
||||
md.registerExtension(self)
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ class PreviewExtension(ExtensionExt):
|
||||
}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def extendMarkdown(self, md: MarkdownExt) -> None: # noqa: N802
|
||||
def extendMarkdown(self, md: MarkdownExt) -> None:
|
||||
"""Register Markdown extension."""
|
||||
md.registerExtension(self)
|
||||
|
||||
@@ -188,6 +188,6 @@ def resolve(processor_path: str, url_path: str) -> str:
|
||||
return posixpath.join(*base_segments)
|
||||
|
||||
|
||||
def makeExtension(**kwargs: Any) -> PreviewExtension: # noqa: N802
|
||||
def makeExtension(**kwargs: Any) -> PreviewExtension:
|
||||
"""Register Markdown extension."""
|
||||
return PreviewExtension(**kwargs)
|
||||
|
||||
@@ -78,13 +78,13 @@ class SearchExtension(ExtensionExt):
|
||||
self.config = {"keep": [set(), "Set of HTML tags to keep in output"]}
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def extendMarkdown(self, md: MarkdownExt) -> None: # noqa: N802
|
||||
def extendMarkdown(self, md: MarkdownExt) -> None:
|
||||
"""Register the PostProcessor with Markdown."""
|
||||
processor = SearchProcessor(md)
|
||||
md.postprocessors.register(processor, "search", 0)
|
||||
|
||||
|
||||
def makeExtension(**kwargs: Any) -> SearchExtension: # noqa: N802
|
||||
def makeExtension(**kwargs: Any) -> SearchExtension:
|
||||
"""Factory function for creating the extension."""
|
||||
return SearchExtension(**kwargs)
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ class ExtensionExt(Extension):
|
||||
`MarkdownExt` instance, which includes the page and configuration.
|
||||
"""
|
||||
|
||||
def extendMarkdown(self, md: MarkdownExt) -> None: # noqa: N802 # ty:ignore[invalid-method-override]
|
||||
def extendMarkdown(self, md: MarkdownExt) -> None: # ty:ignore[invalid-method-override]
|
||||
"""Register Markdown extension."""
|
||||
super().extendMarkdown(md)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user