flowmark 0.2.7

A Markdown auto-formatter for clean diffs and semantic line breaks
Documentation
#!/usr/bin/env -S uv run --script --python 3.14
# /// script
# requires-python = ">=3.14"
# dependencies = [
#   "jinja2>=3.1.6",
#   "strif>=3.0.1",
# ]
# ///
"""Generate the Rust README from shared docs content and a Rust wrapper template."""

from __future__ import annotations

from argparse import ArgumentParser
from pathlib import Path
import re
import tomllib

from jinja2 import Environment, StrictUndefined
from strif import atomic_output_file

UPSTREAM_DOCS_BASE_URL = "https://github.com/jlevy/flowmark/blob/main/docs/"


def parse_args() -> tuple[Path, Path, Path]:
    """Parse command-line arguments and resolve default repo-relative paths."""
    repo_root = Path(__file__).resolve().parents[1]
    parser = ArgumentParser(description="Generate README.md from shared docs + Rust wrapper template.")
    parser.add_argument(
        "--shared-docs",
        "--python-readme",
        dest="shared_docs",
        type=Path,
        default=repo_root / "repos/flowmark/docs/shared/flowmark-readme-shared.md",
        help="Path to canonical shared docs source.",
    )
    parser.add_argument(
        "--template",
        type=Path,
        default=repo_root / "docs/templates/rust-readme-wrapper.md",
        help="Path to Markdown wrapper template rendered with Jinja.",
    )
    parser.add_argument(
        "--output",
        type=Path,
        default=repo_root / "README.md",
        help="Output README path.",
    )
    args = parser.parse_args()
    return args.shared_docs, args.template, args.output


def rewrite_upstream_local_docs_links(markdown: str) -> str:
    """Rewrite local docs links from the shared source to canonical upstream URLs."""
    # Keep the Rust repo's development workflow link local.
    return re.sub(
        r"\]\(docs/(?!development\.md)([^)]+)\)",
        rf"]({UPSTREAM_DOCS_BASE_URL}\1)",
        markdown,
    )


def read_msrv(repo_root: Path) -> str:
    """Read rust-version from Cargo.toml for the MSRV badge."""
    cargo_toml = repo_root / "Cargo.toml"
    if not cargo_toml.exists():
        raise FileNotFoundError(f"missing Cargo.toml at {cargo_toml}")
    metadata = tomllib.loads(cargo_toml.read_text(encoding="utf-8"))
    package = metadata.get("package", {})
    msrv = package.get("rust-version")
    if not isinstance(msrv, str) or not msrv:
        raise ValueError(f"missing [package].rust-version in {cargo_toml}")
    return msrv


def read_parity_version(repo_root: Path) -> str:
    """Read Python parity version from Cargo.toml metadata."""
    cargo_toml = repo_root / "Cargo.toml"
    if not cargo_toml.exists():
        raise FileNotFoundError(f"missing Cargo.toml at {cargo_toml}")
    metadata = tomllib.loads(cargo_toml.read_text(encoding="utf-8"))
    package = metadata.get("package", {})
    parity = package.get("metadata", {}).get("parity", {}).get("version")
    if not isinstance(parity, str) or not parity:
        raise ValueError(f"missing [package.metadata.parity].version in {cargo_toml}")
    return parity


def read_last_sync_date(repo_root: Path) -> str:
    """Read the port last-updated date from docs/port-status.md."""
    port_status = repo_root / "docs/port-status.md"
    if not port_status.exists():
        raise FileNotFoundError(f"missing port status file at {port_status}")
    text = port_status.read_text(encoding="utf-8")
    match = re.search(r"^\*\*Last updated:\*\*\s+([0-9]{4}-[0-9]{2}-[0-9]{2})$", text, re.MULTILINE)
    if not match:
        raise ValueError(f"missing '**Last updated:** YYYY-MM-DD' in {port_status}")
    return match.group(1)


def render_readme(
    template_path: Path,
    shared_docs_body: str,
    msrv: str,
    parity_version: str,
    last_sync_date: str,
) -> str:
    """Render the README wrapper template with transformed shared docs content."""
    environment = Environment(
        autoescape=False,
        undefined=StrictUndefined,
        keep_trailing_newline=True,
    )
    template = environment.from_string(template_path.read_text(encoding="utf-8"))
    rendered = template.render(
        shared_docs_body=shared_docs_body,
        msrv=msrv,
        parity_version=parity_version,
        last_sync_date=last_sync_date,
    )
    if not rendered.endswith("\n"):
        rendered += "\n"
    return rendered


def write_atomic(output_path: Path, content: str) -> None:
    """Write output atomically so partial output files are never created."""
    with atomic_output_file(output_path, make_parents=True) as temp_path:
        Path(temp_path).write_text(content, encoding="utf-8")


def main() -> int:
    """Generate README.md from canonical sources."""
    shared_docs_path, template_path, output_path = parse_args()
    repo_root = Path(__file__).resolve().parents[1]
    if not shared_docs_path.exists():
        raise FileNotFoundError(f"missing shared docs source at {shared_docs_path}")
    if not template_path.exists():
        raise FileNotFoundError(f"missing wrapper template at {template_path}")

    shared_docs_body = shared_docs_path.read_text(encoding="utf-8").rstrip() + "\n"
    shared_docs_body = rewrite_upstream_local_docs_links(shared_docs_body)
    rendered = render_readme(
        template_path,
        shared_docs_body,
        read_msrv(repo_root),
        read_parity_version(repo_root),
        read_last_sync_date(repo_root),
    )
    write_atomic(output_path, rendered)

    print(f"Generated {output_path} from {shared_docs_path} via {template_path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())