From 4ad90fee4c7c3a659fd394dc851641c33cfa3cb2 Mon Sep 17 00:00:00 2001 From: Peisong Xiao Date: Sun, 8 Feb 2026 18:23:02 -0500 Subject: [PATCH] added block html option --- configurations.md | 24 ++++++++++++++++++------ examples.md | 5 +++++ src/config.py | 13 ++++++++++++- src/evaluation.py | 14 ++++++++++++++ src/manifest.py | 20 +++++++++++++++++++- src/markdown_utils.py | 12 +++++++++--- src/models.py | 2 ++ src/scaffold.py | 2 ++ 8 files changed, 81 insertions(+), 11 deletions(-) diff --git a/configurations.md b/configurations.md index 660565d..f302a68 100644 --- a/configurations.md +++ b/configurations.md @@ -14,9 +14,12 @@ Top-level fields: Markdown renderer to use. Allowed values: `default`, `py-gfm`, `pandoc`. 4. `hard_line_breaks` (boolean, optional) If `true`, treat single newlines as hard line breaks. -5. `git_repositories` (array, optional) +5. `block_html` (boolean, optional) + If `true`, wrap HTML in a single Gutenberg HTML block to preserve formatting + in the visual editor. +6. `git_repositories` (array, optional) List of git repositories to manage. Default is an empty list. -6. `directories` (array, optional) +7. `directories` (array, optional) List of non-git directories to manage. Default is an empty list. `git_repositories` entries: @@ -60,9 +63,12 @@ Top-level fields: 5. `hard_line_breaks` (boolean, optional) If `true`, treat single newlines as hard line breaks. If omitted, it inherits from the parent scope. -6. `subdirectories` (object, optional) +6. `block_html` (boolean, optional) + If `true`, wrap HTML in a single Gutenberg HTML block to preserve formatting. + If omitted, it inherits from the parent scope. +7. `subdirectories` (object, optional) Explicit list of subdirectories to traverse. -7. `files` (object, optional) +8. `files` (object, optional) Mapping of Markdown file names to file-level configuration. `categories`, `tags`, `author`, and `subdirectories` objects: @@ -87,6 +93,9 @@ from the parent scope; if specified, it overrides the parent without an explicit The `hard_line_breaks` field inherits implicitly: if omitted, the value is inherited from the parent scope; if specified, it overrides the parent without an explicit `inherit` flag. +The `block_html` field inherits implicitly: if omitted, the value 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. @@ -113,10 +122,13 @@ Each value is an object with the following fields: 6. `hard_line_breaks` (boolean, optional) If `true`, treat single newlines as hard line breaks. If omitted, it inherits from the parent scope. -7. `categories` (object, optional) +7. `block_html` (boolean, optional) + If `true`, wrap HTML in a single Gutenberg HTML block to preserve formatting. + If omitted, it inherits from the parent scope. +8. `categories` (object, optional) Overrides categories for this file. Uses the same `content` and `inherit` fields as the top-level `categories` object. -8. `tags` (object, optional) +9. `tags` (object, optional) Overrides tags for this file. Uses the same `content` and `inherit` fields as the top-level `tags` object. diff --git a/examples.md b/examples.md index a3d2580..9b7bbab 100644 --- a/examples.md +++ b/examples.md @@ -13,6 +13,7 @@ Root directory manifest (`.wp-materialize.json`): "author": { "content": ["editorial"], "inherit": true }, "renderer": "pandoc", "hard_line_breaks": true, + "block_html": true, "subdirectories": { "content": ["design", "notes"], "inherit": true }, "files": { "post.md": { @@ -24,6 +25,7 @@ Root directory manifest (`.wp-materialize.json`): "use_heading_as_title": { "level": 1, "strict": true }, "renderer": "py-gfm", "hard_line_breaks": false, + "block_html": false, "created_on": "2025-01-10 09:30", "last_modified": "2025-02-14 16:45" } @@ -54,6 +56,7 @@ Subdirectory manifest (`design/.wp-materialize.json`): "repo_storage_dir": "/home/user/wp-materialize-repos", "renderer": "default", "hard_line_breaks": false, + "block_html": false, "git_repositories": [], "directories": [ { @@ -73,6 +76,7 @@ Subdirectory manifest (`design/.wp-materialize.json`): "repo_storage_dir": "/home/user/wp-materialize-repos", "renderer": "default", "hard_line_breaks": false, + "block_html": false, "git_repositories": [ { "name": "content-repo", @@ -105,6 +109,7 @@ Subdirectory manifest (`design/.wp-materialize.json`): "repo_storage_dir": "/home/user/wp-materialize-repos", "renderer": "default", "hard_line_breaks": false, + "block_html": false, "git_repositories": [ { "name": "content-repo", diff --git a/src/config.py b/src/config.py index 7675f70..0a164ce 100644 --- a/src/config.py +++ b/src/config.py @@ -31,6 +31,7 @@ class Config: directories: List[DirectorySpec] renderer: Optional[str] hard_line_breaks: bool + block_html: bool def _expect_keys(obj: dict, allowed: set[str], context: str) -> None: @@ -52,7 +53,15 @@ def load_config(path: Path) -> Config: _expect_keys( data, - {"wordpress_root", "repo_storage_dir", "git_repositories", "directories", "renderer", "hard_line_breaks"}, + { + "wordpress_root", + "repo_storage_dir", + "git_repositories", + "directories", + "renderer", + "hard_line_breaks", + "block_html", + }, "config", ) @@ -60,6 +69,7 @@ def load_config(path: Path) -> Config: repo_storage_dir = _require_path(data, "repo_storage_dir", required=True) renderer = _require_renderer(data.get("renderer"), context="config.renderer") hard_line_breaks = _require_bool_optional(data.get("hard_line_breaks"), context="config.hard_line_breaks") + block_html = _require_bool_optional(data.get("block_html"), context="config.block_html") git_repositories = [] for idx, repo in enumerate(data.get("git_repositories", []) or []): @@ -95,6 +105,7 @@ def load_config(path: Path) -> Config: directories=directories, renderer=renderer, hard_line_breaks=False if hard_line_breaks is None else hard_line_breaks, + block_html=False if block_html is None else block_html, ) diff --git a/src/evaluation.py b/src/evaluation.py index d011181..c71ad35 100644 --- a/src/evaluation.py +++ b/src/evaluation.py @@ -23,6 +23,7 @@ class _Context: author: InheritList renderer: Optional[str] hard_line_breaks: bool + block_html: bool subdirectories: InheritList manifest_chain: List[Path] @@ -49,6 +50,7 @@ def evaluate( author=InheritList(), renderer=config.renderer, hard_line_breaks=config.hard_line_breaks, + block_html=config.block_html, subdirectories=InheritList(), manifest_chain=[], ), @@ -151,6 +153,11 @@ def _evaluate_directory( if manifest.hard_line_breaks is not None else context.hard_line_breaks ) + effective_block_html = ( + manifest.block_html + if manifest.block_html is not None + else context.block_html + ) effective_subdirs = _merge_inherit(context.subdirectories, manifest.subdirectories) manifest_chain = context.manifest_chain + [manifest.path] @@ -197,12 +204,18 @@ def _evaluate_directory( if spec.hard_line_breaks is not None else effective_hard_line_breaks ) + resolved_block_html = ( + spec.block_html + if spec.block_html is not None + else effective_block_html + ) html = convert_markdown( markdown_body, context=str(file_path), issues=issues, renderer=resolved_renderer or "default", hard_line_breaks=resolved_hard_line_breaks, + block_html=resolved_block_html, ) if html is None: continue @@ -272,6 +285,7 @@ def _evaluate_directory( author=effective_author, renderer=effective_renderer, hard_line_breaks=effective_hard_line_breaks, + block_html=effective_block_html, subdirectories=effective_subdirs, manifest_chain=manifest_chain, ), diff --git a/src/manifest.py b/src/manifest.py index e6a53e4..4fd1be7 100644 --- a/src/manifest.py +++ b/src/manifest.py @@ -24,7 +24,16 @@ 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", "renderer", "hard_line_breaks", "subdirectories", "files"} + allowed = { + "categories", + "tags", + "author", + "renderer", + "hard_line_breaks", + "block_html", + "subdirectories", + "files", + } extra = set(data.keys()) - allowed if extra: issues.append(ValidationIssue(f"Unexpected keys: {sorted(extra)}", context=str(path))) @@ -35,6 +44,7 @@ def load_manifest(path: Path, issues: list[ValidationIssue]) -> Manifest | None: author = _parse_inherit_list(data.get("author"), issues, f"{path}:author") renderer = _parse_renderer_field(data.get("renderer"), issues, f"{path}:renderer") hard_line_breaks = _parse_bool_field(data.get("hard_line_breaks"), issues, f"{path}:hard_line_breaks") + block_html = _parse_bool_field(data.get("block_html"), issues, f"{path}:block_html") subdirectories = _parse_inherit_list(data.get("subdirectories"), issues, f"{path}:subdirectories") files: Dict[str, FileSpec] = {} @@ -59,6 +69,7 @@ def load_manifest(path: Path, issues: list[ValidationIssue]) -> Manifest | None: "last_modified", "renderer", "hard_line_breaks", + "block_html", } if extra_file: issues.append( @@ -110,6 +121,11 @@ def load_manifest(path: Path, issues: list[ValidationIssue]) -> Manifest | None: issues, f"{path}:{file_name}:hard_line_breaks", ) + block_html_override = _parse_bool_field( + file_cfg.get("block_html"), + issues, + f"{path}:{file_name}:block_html", + ) 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)) @@ -125,6 +141,7 @@ def load_manifest(path: Path, issues: list[ValidationIssue]) -> Manifest | None: last_modified=last_modified, renderer=renderer_override, hard_line_breaks=hard_line_breaks_override, + block_html=block_html_override, ) return Manifest( @@ -134,6 +151,7 @@ def load_manifest(path: Path, issues: list[ValidationIssue]) -> Manifest | None: author=author, renderer=renderer, hard_line_breaks=hard_line_breaks, + block_html=block_html, subdirectories=subdirectories, files=files, ) diff --git a/src/markdown_utils.py b/src/markdown_utils.py index 39a1743..8ae44c2 100644 --- a/src/markdown_utils.py +++ b/src/markdown_utils.py @@ -61,13 +61,19 @@ def convert_markdown( issues: list[ValidationIssue], renderer: str = "default", hard_line_breaks: bool = False, + block_html: bool = False, ) -> str | None: + def wrap_blocks(html: str) -> str: + if not block_html: + return html + return f\"\\n{html}\\n\" + if renderer == "default": try: extensions = ["extra"] if hard_line_breaks: extensions.append("nl2br") - return md_lib.markdown(markdown_text, extensions=extensions, output_format="html5") + return wrap_blocks(md_lib.markdown(markdown_text, extensions=extensions, 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 @@ -85,7 +91,7 @@ def convert_markdown( extensions = [extension_class()] if hard_line_breaks: extensions.append("nl2br") - return md_lib.markdown(markdown_text, extensions=extensions, output_format="html5") + return wrap_blocks(md_lib.markdown(markdown_text, extensions=extensions, 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 @@ -98,7 +104,7 @@ def convert_markdown( capture_output=True, check=True, ) - return result.stdout + return wrap_blocks(result.stdout) except FileNotFoundError as exc: issues.append(ValidationIssue(f"pandoc is not available: {exc}", context=context)) return None diff --git a/src/models.py b/src/models.py index d39b873..64892dc 100644 --- a/src/models.py +++ b/src/models.py @@ -23,6 +23,7 @@ class FileSpec: last_modified: Optional[datetime] renderer: Optional[str] hard_line_breaks: Optional[bool] + block_html: Optional[bool] @dataclass(frozen=True) @@ -33,6 +34,7 @@ class Manifest: author: InheritList renderer: Optional[str] hard_line_breaks: Optional[bool] + block_html: Optional[bool] subdirectories: InheritList files: Dict[str, FileSpec] diff --git a/src/scaffold.py b/src/scaffold.py index bea43ac..b4e6b8d 100644 --- a/src/scaffold.py +++ b/src/scaffold.py @@ -16,6 +16,7 @@ def create_config(path: Path) -> None: "repo_storage_dir": "/path/to/repo-storage", "renderer": "default", "hard_line_breaks": False, + "block_html": False, "git_repositories": [ { "name": "example-repo", @@ -49,6 +50,7 @@ def create_manifest(directory: Path) -> Path: "author": {"content": [], "inherit": True}, "renderer": "default", "hard_line_breaks": False, + "block_html": False, "subdirectories": {"content": [], "inherit": True}, "files": {}, }