added renderer selection (py-gfm as alternative)

This commit is contained in:
2026-02-08 17:47:35 -05:00
parent 122b7ea348
commit 02bc0ce81d
11 changed files with 115 additions and 17 deletions

View File

@@ -29,6 +29,7 @@ class Config:
repo_storage_dir: Path
git_repositories: List[GitRepository]
directories: List[DirectorySpec]
renderer: Optional[str]
def _expect_keys(obj: dict, allowed: set[str], context: str) -> None:
@@ -48,10 +49,11 @@ def load_config(path: Path) -> Config:
if not isinstance(data, dict):
raise ConfigurationError("Config must be a JSON object")
_expect_keys(data, {"wordpress_root", "repo_storage_dir", "git_repositories", "directories"}, "config")
_expect_keys(data, {"wordpress_root", "repo_storage_dir", "git_repositories", "directories", "renderer"}, "config")
wordpress_root = _require_path(data, "wordpress_root", required=True)
repo_storage_dir = _require_path(data, "repo_storage_dir", required=True)
renderer = _require_renderer(data.get("renderer"), context="config.renderer")
git_repositories = []
for idx, repo in enumerate(data.get("git_repositories", []) or []):
@@ -85,6 +87,7 @@ def load_config(path: Path) -> Config:
repo_storage_dir=repo_storage_dir,
git_repositories=git_repositories,
directories=directories,
renderer=renderer,
)
@@ -102,3 +105,14 @@ def _require_path(data: dict, key: str, required: bool) -> Path:
if not isinstance(value, str) or not value.strip():
raise ConfigurationError(f"{key} must be a non-empty string")
return Path(value)
def _require_renderer(value: object, context: str) -> Optional[str]:
if value is None:
return None
if not isinstance(value, str) or not value.strip():
raise ConfigurationError(f"{context} must be a non-empty string")
renderer = value.strip()
if renderer not in {"default", "py-gfm"}:
raise ConfigurationError(f"{context} must be one of: default, py-gfm")
return renderer

View File

@@ -21,6 +21,7 @@ class _Context:
categories: InheritList
tags: InheritList
author: InheritList
renderer: Optional[str]
subdirectories: InheritList
manifest_chain: List[Path]
@@ -45,6 +46,7 @@ def evaluate(
categories=InheritList(),
tags=InheritList(),
author=InheritList(),
renderer=config.renderer,
subdirectories=InheritList(),
manifest_chain=[],
),
@@ -141,6 +143,7 @@ def _evaluate_directory(
effective_categories = _merge_inherit(context.categories, manifest.categories)
effective_tags = _merge_inherit(context.tags, manifest.tags)
effective_author = _merge_inherit(context.author, manifest.author)
effective_renderer = manifest.renderer if manifest.renderer is not None else context.renderer
effective_subdirs = _merge_inherit(context.subdirectories, manifest.subdirectories)
manifest_chain = context.manifest_chain + [manifest.path]
@@ -181,7 +184,13 @@ def _evaluate_directory(
resolved_tags = _normalize_list(resolved_tags, "tag", str(file_path), issues)
resolved_author = _resolve_author(effective_author.content, str(file_path), issues)
html = convert_markdown(markdown_body, context=str(file_path), issues=issues)
resolved_renderer = spec.renderer if spec.renderer is not None else effective_renderer
html = convert_markdown(
markdown_body,
context=str(file_path),
issues=issues,
renderer=resolved_renderer or "default",
)
if html is None:
continue
@@ -248,6 +257,7 @@ def _evaluate_directory(
categories=effective_categories,
tags=effective_tags,
author=effective_author,
renderer=effective_renderer,
subdirectories=effective_subdirs,
manifest_chain=manifest_chain,
),

View File

@@ -24,7 +24,7 @@ def load_manifest(path: Path, issues: list[ValidationIssue]) -> Manifest | None:
issues.append(ValidationIssue("Manifest must be a JSON object", context=str(path)))
return None
allowed = {"categories", "tags", "author", "subdirectories", "files"}
allowed = {"categories", "tags", "author", "renderer", "subdirectories", "files"}
extra = set(data.keys()) - allowed
if extra:
issues.append(ValidationIssue(f"Unexpected keys: {sorted(extra)}", context=str(path)))
@@ -33,6 +33,7 @@ def load_manifest(path: Path, issues: list[ValidationIssue]) -> Manifest | None:
categories = _parse_inherit_list(data.get("categories"), issues, f"{path}:categories")
tags = _parse_inherit_list(data.get("tags"), issues, f"{path}:tags")
author = _parse_inherit_list(data.get("author"), issues, f"{path}:author")
renderer = _parse_renderer_field(data.get("renderer"), issues, f"{path}:renderer")
subdirectories = _parse_inherit_list(data.get("subdirectories"), issues, f"{path}:subdirectories")
files: Dict[str, FileSpec] = {}
@@ -48,7 +49,15 @@ def load_manifest(path: Path, issues: list[ValidationIssue]) -> Manifest | None:
if not isinstance(file_cfg, dict):
issues.append(ValidationIssue(f"{file_name} must be an object", context=str(path)))
continue
extra_file = set(file_cfg.keys()) - {"title", "use_heading_as_title", "categories", "tags", "created_on", "last_modified"}
extra_file = set(file_cfg.keys()) - {
"title",
"use_heading_as_title",
"categories",
"tags",
"created_on",
"last_modified",
"renderer",
}
if extra_file:
issues.append(
ValidationIssue(f"{file_name} has unexpected keys: {sorted(extra_file)}", context=str(path))
@@ -93,6 +102,7 @@ def load_manifest(path: Path, issues: list[ValidationIssue]) -> Manifest | None:
tags_override = _parse_inherit_list(file_cfg.get("tags"), issues, f"{path}:{file_name}:tags")
created_on = _parse_datetime_field(file_cfg.get("created_on"), issues, f"{path}:{file_name}:created_on")
last_modified = _parse_datetime_field(file_cfg.get("last_modified"), issues, f"{path}:{file_name}:last_modified")
renderer_override = _parse_renderer_field(file_cfg.get("renderer"), issues, f"{path}:{file_name}:renderer")
if created_on and last_modified and last_modified < created_on:
issues.append(
ValidationIssue("last_modified cannot be earlier than created_on", context=str(path))
@@ -106,6 +116,7 @@ def load_manifest(path: Path, issues: list[ValidationIssue]) -> Manifest | None:
tags=tags_override,
created_on=created_on,
last_modified=last_modified,
renderer=renderer_override,
)
return Manifest(
@@ -113,6 +124,7 @@ def load_manifest(path: Path, issues: list[ValidationIssue]) -> Manifest | None:
categories=categories,
tags=tags,
author=author,
renderer=renderer,
subdirectories=subdirectories,
files=files,
)
@@ -153,3 +165,16 @@ def _parse_datetime_field(value: object, issues: list[ValidationIssue], context:
except ValueError:
issues.append(ValidationIssue("Invalid datetime format (expected YYYY-MM-DD hh:mm)", context=context))
return None
def _parse_renderer_field(value: object, issues: list[ValidationIssue], context: str) -> str | None:
if value is None:
return None
if not isinstance(value, str) or not value.strip():
issues.append(ValidationIssue("Must be a non-empty string", context=context))
return None
renderer = value.strip()
if renderer not in {"default", "py-gfm"}:
issues.append(ValidationIssue("Must be one of: default, py-gfm", context=context))
return None
return renderer

View File

@@ -54,9 +54,32 @@ def _promote_headings(text: str) -> str:
return "\n".join(promoted_lines)
def convert_markdown(markdown_text: str, context: str, issues: list[ValidationIssue]) -> str | None:
try:
return md_lib.markdown(markdown_text, extensions=["extra"], output_format="html5")
except Exception as exc: # pragma: no cover - depends on markdown internals
issues.append(ValidationIssue(f"Markdown conversion failed: {exc}", context=context))
return None
def convert_markdown(
markdown_text: str,
context: str,
issues: list[ValidationIssue],
renderer: str = "default",
) -> str | None:
if renderer == "default":
try:
return md_lib.markdown(markdown_text, extensions=["extra"], output_format="html5")
except Exception as exc: # pragma: no cover - depends on markdown internals
issues.append(ValidationIssue(f"Markdown conversion failed: {exc}", context=context))
return None
if renderer == "py-gfm":
try:
import mdx_gfm
except Exception as exc: # pragma: no cover - dependency missing
issues.append(ValidationIssue(f"py-gfm is not available: {exc}", context=context))
return None
extension_class = getattr(mdx_gfm, "GithubFlavoredMarkdownExtension", None)
if extension_class is None:
issues.append(ValidationIssue("py-gfm extension not found: GithubFlavoredMarkdownExtension", context=context))
return None
try:
return md_lib.markdown(markdown_text, extensions=[extension_class()], output_format="html5")
except Exception as exc: # pragma: no cover - depends on markdown internals
issues.append(ValidationIssue(f"Markdown conversion failed: {exc}", context=context))
return None
issues.append(ValidationIssue(f"Unknown renderer: {renderer}", context=context))
return None

View File

@@ -21,6 +21,7 @@ class FileSpec:
tags: Optional[InheritList]
created_on: Optional[datetime]
last_modified: Optional[datetime]
renderer: Optional[str]
@dataclass(frozen=True)
@@ -29,6 +30,7 @@ class Manifest:
categories: InheritList
tags: InheritList
author: InheritList
renderer: Optional[str]
subdirectories: InheritList
files: Dict[str, FileSpec]

View File

@@ -14,6 +14,7 @@ def create_config(path: Path) -> None:
payload = {
"wordpress_root": "/path/to/wordpress",
"repo_storage_dir": "/path/to/repo-storage",
"renderer": "default",
"git_repositories": [
{
"name": "example-repo",
@@ -45,6 +46,7 @@ def create_manifest(directory: Path) -> Path:
"categories": {"content": [], "inherit": True},
"tags": {"content": [], "inherit": True},
"author": {"content": [], "inherit": True},
"renderer": "default",
"subdirectories": {"content": [], "inherit": True},
"files": {},
}