#!/usr/bin/env python3 """Generate Typst listing wrappers for Appendix A from Wallenc source tree.""" from __future__ import annotations import argparse import hashlib import sys from pathlib import Path try: import yaml except ImportError: yaml = None # type: ignore def load_config(path: Path) -> dict: text = path.read_text(encoding="utf-8") if yaml is None: raise SystemExit("PyYAML required: pip install pyyaml") return yaml.safe_load(text) def path_hash(rel: str) -> str: return hashlib.sha256(rel.encode()).hexdigest()[:8] def lang_for(path: Path, ext_map: dict, name_map: dict) -> str: if path.name in name_map: return name_map[path.name] return ext_map.get(path.suffix.lower(), "text") def should_skip(path: Path, parts: tuple[str, ...], cfg: dict) -> bool: exclude_dirs = set(cfg.get("exclude_dirs", [])) if any(p in exclude_dirs for p in parts): return True s = path.as_posix() for g in cfg.get("exclude_globs", []): g = g.replace("**/", "") if g.startswith("**/"): g = g[3:] if g.endswith("/**"): if g[:-3] in parts: return True elif g.startswith("*."): if s.endswith(g[1:]): return True elif g in s: return True return False def collect_files(scan_root: Path, cfg: dict) -> list[Path]: ext_map: dict = cfg.get("include_extensions", {}) name_map: dict = cfg.get("include_filenames", {}) allowed_suffixes = set(ext_map.keys()) allowed_names = set(name_map.keys()) files: list[Path] = [] for p in scan_root.rglob("*"): if not p.is_file(): continue rel_parts = p.relative_to(scan_root).parts if should_skip(p, rel_parts, cfg): continue if p.name in allowed_names or p.suffix.lower() in allowed_suffixes: files.append(p) return sorted(files, key=lambda x: x.as_posix()) def module_key(rel: Path, cfg: dict) -> str: order = cfg.get("modules_order", []) parts = rel.parts if rel.name in {Path(f).name for f in cfg.get("root_build_files", [])}: return "root" if parts and parts[0] in order: return parts[0] return "other" def group_files(files: list[Path], scan_root: Path, cfg: dict) -> dict[str, list[Path]]: groups: dict[str, list[Path]] = {} root_files = {Path(f).name for f in cfg.get("root_build_files", [])} for f in files: rel = f.relative_to(scan_root) if rel.name in root_files or len(rel.parts) == 1 and rel.suffix in {".kts", ".properties"}: groups.setdefault("root", []).append(f) continue key = module_key(rel, cfg) groups.setdefault(key, []).append(f) order = cfg.get("modules_order", []) + ["other"] return {k: groups[k] for k in order if k in groups} def typst_escape_path(rel: str) -> str: return rel.replace("\\", "/") def write_listing( out_path: Path, rel_from_report: str, caption: str, label: str, lang: str, ) -> None: rel_typ = typst_escape_path(rel_from_report) # Подпись перед кодом: figure.caption(position: top) в теле (placement: top — про float, не порядок). content = ( f'#let lst-body-{label} = read("{rel_typ}")\n' f"#figure(\n" f" [\n" f" #figure.caption(position: top)[{caption}]\n" f" #block(breakable: true)[\n" f" #raw(lst-body-{label}, lang: \"{lang}\", block: true)\n" f" ]\n" f" ],\n" f" supplement: [Листинг],\n" f" gap: 0.4em,\n" f") \n\n" ) out_path.write_text(content, encoding="utf-8") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--dry-run", action="store_true") parser.add_argument("--config", default=None) args = parser.parse_args() script_dir = Path(__file__).resolve().parent report_dir = script_dir.parent config_path = Path(args.config) if args.config else report_dir / "listings" / "listings.config.yaml" cfg = load_config(config_path) scan_root = (report_dir / cfg.get("scan_root", "..")).resolve() generated = report_dir / "listings" / "generated" ext_map: dict = cfg.get("include_extensions", {}) name_map: dict = cfg.get("include_filenames", {}) files = collect_files(scan_root, cfg) if args.dry_run: for f in files: print(f.relative_to(scan_root).as_posix()) print(f"Total: {len(files)}", file=sys.stderr) return 0 generated.mkdir(parents=True, exist_ok=True) groups = group_files(files, scan_root, cfg) listing_paths: list[str] = [] module_includes: list[str] = [] for mod, mod_files in groups.items(): mod_listings: list[str] = [] for f in mod_files: rel_repo = f.relative_to(scan_root).as_posix() # Paths relative to listings/generated/*.typ (typst compile --root ..) rel_report = Path("../../..") / rel_repo rel_report_str = rel_report.as_posix() hid = path_hash(rel_repo) label = hid cap = f"Исходный файл `{rel_repo}`" lang = lang_for(f, ext_map, name_map) listing_name = f"listing-{hid}.typ" listing_path = generated / listing_name write_listing(listing_path, rel_report_str, cap, label, lang) mod_listings.append(listing_name) listing_paths.append(listing_name) mod_file = generated / f"module-{mod}.typ" with mod_file.open("w", encoding="utf-8") as mf: title = "Система сборки" if mod == "root" else f"Модуль :{mod}" mf.write(f"== {title}\n\n") if cfg.get("pagebreak_per_module") and mod != "root": mf.write("#pagebreak(weak: true)\n\n") for ln in mod_listings: mf.write(f'#include "{ln}"\n') module_includes.append(f"module-{mod}.typ") index = generated / "_index.typ" with index.open("w", encoding="utf-8") as ix: ix.write(f"// Generated listings: {len(files)} files\n\n") appendix = generated / "appendix-a.typ" with appendix.open("w", encoding="utf-8") as ap: ap.write("// AUTO-GENERATED by gen_listings.py — do not edit\n\n") ap.write("#set figure(gap: 0.4em)\n\n") for mi in module_includes: ap.write(f'#include "{mi}"\n') print(f"Generated {len(files)} listings in {generated}", file=sys.stderr) return 0 if __name__ == "__main__": raise SystemExit(main())