from __future__ import annotations import re from pathlib import Path _FILE_PLACEHOLDER_RE = re.compile(r"\{\{file:([^}]+)\}\}") def expand_file_placeholders( text: str, *, workspace_root: Path, seen: set[Path] | None = None, ) -> str: workspace_root = workspace_root.resolve() active = set() if seen is None else set(seen) def replace(match: re.Match[str]) -> str: raw_ref = match.group(1).strip() include_path = Path(raw_ref) if not include_path.is_absolute(): include_path = workspace_root / include_path include_path = include_path.resolve() if include_path in active: raise ValueError(f"cyclic {{file:...}} include detected at {include_path}") included = include_path.read_text(encoding="utf-8") return expand_file_placeholders( included, workspace_root=workspace_root, seen={*active, include_path}, ) return _FILE_PLACEHOLDER_RE.sub(replace, text) def materialize_expanded_card( card_path: Path, *, workspace_root: Path, out_dir: Path, ) -> Path: card_path = card_path.resolve() expanded = expand_file_placeholders( card_path.read_text(encoding="utf-8"), workspace_root=workspace_root, seen={card_path}, ) out_dir.mkdir(parents=True, exist_ok=True) output_path = out_dir / f".{card_path.stem}.expanded{card_path.suffix}" output_path.write_text(expanded, encoding="utf-8") return output_path