#!/usr/bin/env python3 """ Generates the patches section of README.md from patches-list.json and injects it between / markers. Spoilers are expanded (open by default) if: 1. Total patch count <= AUTO_EXPAND_THRESHOLD. 2. The README marker explicitly says: python3 generate_patches_readme.py [patches-list.json] [README.md] """ import json import re import sys import os from pathlib import Path if len(sys.argv) < 3: print("Usage: generate_patches_readme.py [json] [readme]") sys.exit(1) repo_full = sys.argv[1] branch = sys.argv[2] json_path = Path(sys.argv[3]) if len(sys.argv) > 3 else Path("patches-list.json") readme_path = Path(sys.argv[4]) if len(sys.argv) > 4 else Path("README.md") if "/" not in repo_full: raise ValueError(f"Invalid repo format: {repo_full} (expected owner/repo)") owner, repo = repo_full.split("/", 1) with open(json_path, encoding="utf-8") as f: data = json.load(f) def pkg_emoji(pkg): """Return a standard package emoji regardless of the package name.""" return "๐Ÿ“ฆ" # Group patches by package; patches with no compatiblePackages are universal. # JSON structure: compatiblePackages is a list of objects with # { packageName, name, targets: [{ version, isExperimental, description }] } by_pkg = {} # packageName -> { name, emoji, patches, targets } universal = {} for patch in data["patches"]: cp = patch.get("compatiblePackages") if not cp: # Deduplicate universal patches by name if patch["name"] not in universal: universal[patch["name"]] = patch continue for pkg_entry in cp: pkg = pkg_entry["packageName"] name = pkg_entry.get("name") or pkg # fall back to package name if no label if pkg not in by_pkg: by_pkg[pkg] = { "name": name, "emoji": pkg_emoji(pkg), "patches": {}, "targets": pkg_entry.get("targets", []), } # Deduplicate patches that appear across multiple packages if patch["name"] not in by_pkg[pkg]["patches"]: by_pkg[pkg]["patches"][patch["name"]] = patch def anchor(name): """Convert a patch name to a GitHub-compatible anchor slug.""" return re.sub(r"-+", "-", re.sub(r"[^a-z0-9]+", "-", name.lower())).strip("-") def patches_table(patches): """Render a sorted markdown table of patches with name, description, and options.""" rows = [ "| ๐Ÿ’Š Patch | ๐Ÿ“œ Description | โš™๏ธ Options |", "|----------|----------------|-----------|", ] for p in sorted(patches, key=lambda x: x["name"]): a = anchor(p["name"]) options = p.get("options") or [] if options: # Show only option titles as a bullet list parts = [opt.get("title") or opt.get("key") or "" for opt in options] opts_cell = "
".join(f"โ€ข {t}" for t in parts) else: opts_cell = "" desc = (p.get("description") or "").replace("\n", "
") rows.append(f"| [{p['name']}](#{a}) | {desc} | {opts_cell} |") return "\n".join(rows) def versions_table(targets): """Render a markdown table of supported versions. Experimental versions get a ๐Ÿงช prefix. Versions with a description get it shown in a second row below. """ if not targets: return "" cells = [] for t in targets: ver = t["version"] if ver is None: continue label = f"๐Ÿงช {ver}" if t.get("isExperimental") else ver cells.append(label) if not cells: return "" header = "| " + " | ".join(cells) + " |" sep = "| " + " | ".join(":---:" for _ in cells) + " |" rows = [header, sep] # Optional description row โ€” only rendered if at least one target has one descs = [(t.get("description") or "").replace("\n", "
") for t in targets] if any(descs): rows.append("| " + " | ".join(descs) + " |") return "\n".join(rows) def spoiler(label, count, targets, tbl, expanded=False): """Wrap a patches table in a
spoiler with a versions sub-table. If expanded=True, the spoiler is open by default (for repos with few patches). """ noun = "patch" if count == 1 else "patches" vtbl = versions_table(targets) versions_section = f"**๐ŸŽฏ Supported versions:**\n\n{vtbl}\n\n" if vtbl else "" tag = "
" if expanded else "
" return f"""{tag} {label}  โ€ข  {count} {noun}
{versions_section}{tbl}
""" def build_content(expanded=False): """Build the full generated patches section.""" lines = [ f"> **[v{ver}](https://github.com/{owner}/{repo}/releases/tag/v{ver})**" f"  โ€ข  `{branch}`  โ€ข  " f"{total} patches total" ] # One spoiler per app, in the order they appear in the JSON for pkg, entry in by_pkg.items(): patches = list(entry["patches"].values()) label = f"{entry['emoji']} {entry['name']}" lines.append(spoiler(label, len(patches), entry["targets"], patches_table(patches), expanded)) lines.append("") # Universal patches (no specific app) if universal: uni_patches = list(universal.values()) noun = "patch" if len(uni_patches) == 1 else "patches" tag = "
" if expanded else "
" lines.append(f"""{tag} ๐ŸŒ Universal  โ€ข  {len(uni_patches)} {noun}
{patches_table(uni_patches)}
""") lines.append("") return "\n".join(lines) # Build and inject raw_ver = data["version"] # Strip leading "v" if present ver = raw_ver.lstrip("v") total = sum(len(e["patches"]) for e in by_pkg.values()) + len(universal) readme = readme_path.read_text(encoding="utf-8") # Marker pattern โ€” matches both and START_PATTERN = r"" END_MARKER = "" marker_match = re.search(START_PATTERN, readme) if not marker_match or END_MARKER not in readme: # Fallback: print to stdout so CI can catch the issue print(build_content(expanded=False)) sys.stderr.write( f"โš ๏ธ Markers / {END_MARKER} not found in {readme_path}. " "Printed to stdout instead.\n" ) sys.exit(1) actual_start = marker_match.group(0) # Auto-expand threshold AUTO_EXPAND_THRESHOLD = 20 # Spoilers are expanded if: # 1. Total patch count is small (โ‰ค AUTO_EXPAND_THRESHOLD) # with only a few patches where collapsing adds no benefit. # 2. The README marker explicitly requests it: expanded = ( total <= AUTO_EXPAND_THRESHOLD or "EXPANDED" in actual_start ) generated = build_content(expanded=expanded) # Replace template links if present readme = readme.replace("https://morphe.software/add-source?github=xyz-user/xyz-patches", f"https://morphe.software/add-source?github={repo_full}") readme = readme.replace("https://github.com/xyz-user/xyz-patches", f"https://github.com/{repo_full}") new_readme = re.sub( rf"{START_PATTERN}.*?{re.escape(END_MARKER)}", f"{actual_start}\n{generated}\n{END_MARKER}", readme, flags=re.DOTALL, ) readme_path.write_text(new_readme, encoding="utf-8") print(f"โœ… Injected patches section into {readme_path} (v{ver}, branch={branch}, {total} patches, expanded={expanded})")