feat: pelican plugin
This commit is contained in:
parent
970e0da9bc
commit
370fdfb067
33 changed files with 2274 additions and 0 deletions
127
pelican-mews/pelican/plugins/mews/mews.py
Normal file
127
pelican-mews/pelican/plugins/mews/mews.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
"""Make Pelican output Mews Profile pages.
|
||||
|
||||
Every .xhtml file Pelican writes is brought inside the subset the
|
||||
bundled Mews DTD defines: disallowed markup is stripped (or, with
|
||||
MEWS_STRICT, fails the build), the XHTML namespace is ensured, and the
|
||||
page is re-serialized as well-formed UTF-8 XML. The plugin also copies the
|
||||
Mews stylesheet into the output tree and hands themes a MEWS_CSS global
|
||||
and the mews/head.html partial.
|
||||
|
||||
Settings:
|
||||
|
||||
MEWS_STRICT Fail the build on disallowed content instead of stripping
|
||||
it. Defaults to False. Removals are always logged.
|
||||
MEWS_CSS_DIR Directory below the output root for the stylesheet. Defaults
|
||||
to "css"; themes get the path as the MEWS_CSS global.
|
||||
MEWS_OUTPUT_PATH Put the finished pages in this folder instead of
|
||||
OUTPUT_PATH, so a normal blog can keep its own output tree.
|
||||
Unset by default. The stylesheet is copied there too, and the
|
||||
.xhtml file Pelican left in OUTPUT_PATH is removed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib.resources import files
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from pelican import signals
|
||||
|
||||
from .subset import (
|
||||
MewsSubsetError,
|
||||
element_name,
|
||||
enforce_subset,
|
||||
ensure_xhtml_root,
|
||||
parse_allowlist,
|
||||
parse_page,
|
||||
serialize,
|
||||
)
|
||||
|
||||
__all__ = ("register",)
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CSS_NAME = "mews-0.1.css"
|
||||
DEFAULT_CSS_DIR = "css"
|
||||
XHTML_SUFFIX = ".xhtml"
|
||||
|
||||
ALLOWLIST = parse_allowlist(
|
||||
files(__package__).joinpath("dtd", "mews.dtd").read_text(encoding="utf-8")
|
||||
)
|
||||
|
||||
# content_written carries no settings, so the handler reads them from here;
|
||||
# refreshed by the initialized signal, which fires before every build.
|
||||
_settings: dict = {}
|
||||
|
||||
|
||||
def _on_initialized(pelican) -> None:
|
||||
_settings.clear()
|
||||
_settings.update(pelican.settings)
|
||||
overrides = pelican.settings.setdefault("THEME_TEMPLATES_OVERRIDES", [])
|
||||
templates = str(files(__package__).joinpath("templates"))
|
||||
if templates not in overrides:
|
||||
overrides.append(templates)
|
||||
|
||||
|
||||
def _on_generator_init(generator) -> None:
|
||||
css_dir = generator.settings.get("MEWS_CSS_DIR", DEFAULT_CSS_DIR)
|
||||
generator.env.globals["MEWS_CSS"] = f"{css_dir}/{CSS_NAME}"
|
||||
|
||||
|
||||
def _on_content_written(path, **_) -> None:
|
||||
file = Path(path)
|
||||
if file.suffix != XHTML_SUFFIX:
|
||||
return
|
||||
try:
|
||||
tree, doctype = parse_page(file.read_bytes())
|
||||
except etree.XMLSyntaxError as error:
|
||||
message = f"{file} is not well-formed XML: {error}"
|
||||
raise MewsSubsetError(message) from error
|
||||
if element_name(tree.getroot().tag) != "html":
|
||||
LOGGER.warning("%s: root element is not <html>; leaving it unchanged", file)
|
||||
return
|
||||
tree = ensure_xhtml_root(tree)
|
||||
strict = bool(_settings.get("MEWS_STRICT", False))
|
||||
enforce_subset(tree, ALLOWLIST, strict=strict, source=str(file), log=LOGGER.warning)
|
||||
data = serialize(tree, doctype)
|
||||
parallel = _settings.get("MEWS_OUTPUT_PATH")
|
||||
if not parallel:
|
||||
file.write_bytes(data)
|
||||
return
|
||||
target = Path(parallel) / os.path.relpath(file, _settings["OUTPUT_PATH"])
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(data)
|
||||
file.unlink()
|
||||
_prune_empty_dirs(file, Path(_settings["OUTPUT_PATH"]).resolve())
|
||||
LOGGER.info("moved %s to %s", file, target)
|
||||
|
||||
|
||||
def _prune_empty_dirs(path: Path, root: Path) -> None:
|
||||
"""Remove directories the move left empty, up to the output root."""
|
||||
empty = path.parent.resolve()
|
||||
while empty not in (root, empty.parent) and not any(empty.iterdir()):
|
||||
empty.rmdir()
|
||||
empty = empty.parent
|
||||
|
||||
|
||||
def _on_finalized(pelican) -> None:
|
||||
css_dir = pelican.settings.get("MEWS_CSS_DIR", DEFAULT_CSS_DIR)
|
||||
root = pelican.settings.get("MEWS_OUTPUT_PATH") or pelican.settings["OUTPUT_PATH"]
|
||||
target = Path(root) / css_dir / CSS_NAME
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
source = files(__package__).joinpath("static", CSS_NAME)
|
||||
with source.open("rb") as src, target.open("wb") as dst:
|
||||
shutil.copyfileobj(src, dst)
|
||||
LOGGER.info("wrote %s", target)
|
||||
|
||||
|
||||
def register() -> None:
|
||||
"""Connect the plugin to Pelican's signals."""
|
||||
signals.initialized.connect(_on_initialized)
|
||||
signals.generator_init.connect(_on_generator_init)
|
||||
signals.content_written.connect(_on_content_written)
|
||||
signals.finalized.connect(_on_finalized)
|
||||
Loading…
Add table
Add a link
Reference in a new issue