Черновик ПЗ
This commit is contained in:
196
Report/scripts/gen_listings.py
Normal file
196
Report/scripts/gen_listings.py
Normal file
@@ -0,0 +1,196 @@
|
||||
#!/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") <lst-{label}>\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())
|
||||
Reference in New Issue
Block a user