diff --git a/README.md b/README.md index ca34fab..c08c86c 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ Each managed directory must contain a `.wp-materialize.json` manifest. See `conf 1. `wp` CLI must be installed and available in PATH for `apply`. 2. `local` does not require `wp`. +3. `pandoc` must be installed and available in PATH when using `renderer: "pandoc"`. Install dependencies: diff --git a/configurations.md b/configurations.md index efcb8bd..d8759b1 100644 --- a/configurations.md +++ b/configurations.md @@ -11,7 +11,7 @@ Top-level fields: 2. `repo_storage_dir` (string, required) Directory where git repositories are cloned or updated. 3. `renderer` (string, optional) - Markdown renderer to use. Allowed values: `default`, `py-gfm`. + Markdown renderer to use. Allowed values: `default`, `py-gfm`, `pandoc`. 4. `git_repositories` (array, optional) List of git repositories to manage. Default is an empty list. 5. `directories` (array, optional) @@ -53,7 +53,7 @@ Top-level fields: 3. `author` (object, optional) Inherited author for this directory and its children. Must resolve to a single author. 4. `renderer` (string, optional) - Markdown renderer to use for this directory. Allowed values: `default`, `py-gfm`. + Markdown renderer to use for this directory. Allowed values: `default`, `py-gfm`, `pandoc`. If omitted, it inherits from the parent scope. 5. `subdirectories` (object, optional) Explicit list of subdirectories to traverse. @@ -80,6 +80,11 @@ The `renderer` field inherits implicitly: if omitted, the renderer is inherited from the parent scope; if specified, it overrides the parent without an explicit `inherit` flag. +Renderer dependencies: +1. `default` uses the Python `Markdown` library. +2. `py-gfm` requires the `py_gfm` package (imported as `mdx_gfm`). +3. `pandoc` requires the `pandoc` binary to be available on PATH. + `files` entries: Each key is a Markdown file name (relative to the manifest directory). @@ -95,7 +100,7 @@ Each value is an object with the following fields: 4. `last_modified` (string, optional) Manual override for the post modified time in `YYYY-MM-DD hh:mm` format. 5. `renderer` (string, optional) - Markdown renderer to use for this file. Allowed values: `default`, `py-gfm`. + Markdown renderer to use for this file. Allowed values: `default`, `py-gfm`, `pandoc`. If omitted, it inherits from the parent scope. 6. `categories` (object, optional) Overrides categories for this file. Uses the same `content` and `inherit` fields diff --git a/examples.md b/examples.md index 163d384..c607b02 100644 --- a/examples.md +++ b/examples.md @@ -11,7 +11,7 @@ Root directory manifest (`.wp-materialize.json`): "categories": { "content": ["Systems", "Infrastructure"], "inherit": true }, "tags": { "content": ["automation", "wordpress"], "inherit": true }, "author": { "content": ["editorial"], "inherit": true }, - "renderer": "py-gfm", + "renderer": "pandoc", "subdirectories": { "content": ["design", "notes"], "inherit": true }, "files": { "post.md": { @@ -21,7 +21,7 @@ Root directory manifest (`.wp-materialize.json`): }, "essay.md": { "use_heading_as_title": { "level": 1, "strict": true }, - "renderer": "default", + "renderer": "py-gfm", "created_on": "2025-01-10 09:30", "last_modified": "2025-02-14 16:45" } diff --git a/src/config.py b/src/config.py index 6471a4d..56ab02f 100644 --- a/src/config.py +++ b/src/config.py @@ -113,6 +113,6 @@ def _require_renderer(value: object, context: str) -> Optional[str]: 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") + if renderer not in {"default", "py-gfm", "pandoc"}: + raise ConfigurationError(f"{context} must be one of: default, py-gfm, pandoc") return renderer diff --git a/src/manifest.py b/src/manifest.py index d2a65a7..0ab44a1 100644 --- a/src/manifest.py +++ b/src/manifest.py @@ -174,7 +174,7 @@ def _parse_renderer_field(value: object, issues: list[ValidationIssue], context: 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)) + if renderer not in {"default", "py-gfm", "pandoc"}: + issues.append(ValidationIssue("Must be one of: default, py-gfm, pandoc", context=context)) return None return renderer diff --git a/src/markdown_utils.py b/src/markdown_utils.py index 240b954..e51533f 100644 --- a/src/markdown_utils.py +++ b/src/markdown_utils.py @@ -3,6 +3,7 @@ from __future__ import annotations import re import markdown as md_lib +import subprocess from .errors import ValidationIssue @@ -81,5 +82,22 @@ def convert_markdown( 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 == "pandoc": + try: + result = subprocess.run( + ["pandoc", "--from=markdown", "--to=html5"], + input=markdown_text, + text=True, + capture_output=True, + check=True, + ) + return result.stdout + except FileNotFoundError as exc: + issues.append(ValidationIssue(f"pandoc is not available: {exc}", context=context)) + return None + except subprocess.CalledProcessError as exc: + stderr = exc.stderr.strip() if exc.stderr else "" + issues.append(ValidationIssue(f"Pandoc conversion failed: {stderr}", context=context)) + return None issues.append(ValidationIssue(f"Unknown renderer: {renderer}", context=context)) return None