#!/usr/bin/env python3 """Generate UGI_SPEC_v0.md from ugi_registry_v0.json.""" import json import os import sys def load_registry(path): with open(path, "r") as f: return json.load(f) def gen_title(spec): lines = [] lines.append(f"# {spec['name']} — {spec['full_name']}") lines.append("") lines.append( f"**Version: {spec['version']} ({spec['status'].title()}) · " f"{spec['date']} · {spec['license']} License**" ) lines.append("") lines.append("---") lines.append("") return "\n".join(lines) def gen_overview(spec): lines = [] lines.append("## Overview") lines.append("") lines.append(spec["description"]) lines.append("") lines.append("It descends from two predecessors:") lines.append("") for p in spec["predecessors"]: line = f"- [{p['name']}]({p['url']}) by {p['author']} ({p['years']})" if "continuation" in p: c = p["continuation"] line += f", with its [{c['name']}]({c['url']}) by {c['author']} ({c['years']})" lines.append(line) lines.append("") lines.append( "UGI takes the cultural breadth of the Geek Code, the numeric compactness " "of the Hacker Key, and adds URI safety, modern fields, and extensibility." ) lines.append("") lines.append("### Design goals") lines.append("") lines.append("1. Single-letter field codes — 24 active, 2 reserved") lines.append("2. Octal (0–7) rating scale") lines.append("3. URI-safe characters only — no percent-encoding needed") lines.append("4. Dual format — URI for machines, block for humans") lines.append("5. Case-insensitive") lines.append("6. Extensible via `~` custom sub-IDs") lines.append("") lines.append("---") lines.append("") return "\n".join(lines) def gen_formats(fmt): lines = [] lines.append("## Formats") lines.append("") lines.append("### URI") lines.append("") lines.append("```") lines.append(fmt["uri"]["template"]) lines.append("```") lines.append("") lines.append("### Block") lines.append("") lines.append("```") lines.append(fmt["block"]["header"]) lines.append(fmt["block"]["first_line"]) lines.append(" ...") lines.append(fmt["block"]["footer"]) lines.append("```") lines.append("") lines.append( "In block format, field codes are repeated per sub-ID for readability: " "`Ppy6 Prs5` instead of `ppy6rs5`. Long-form sub-ID aliases (noted in " "registries) are permitted in block format." ) lines.append("") lines.append("### Conversion") lines.append("") lines.append( "Block → URI: remove header/footer, merge repeated field codes, " "replace spaces with commas, prepend `ugi:`." ) lines.append("") lines.append( "URI → Block: split on commas, expand merged sub-IDs by repeating " "field codes, add header/footer." ) lines.append("") lines.append( "No modifier or character changes are needed — both formats use identical symbols." ) lines.append("") lines.append("### Mandatory fields") lines.append("") mand = ", ".join(f"`{f}`" for f in fmt["mandatory_fields"]) lines.append( f"`@handle` and {mand} (geek type) are required. All others are optional." ) lines.append("") lines.append("---") lines.append("") return "\n".join(lines) def gen_grammar(fields): lines = [] lines.append("## Grammar") lines.append("") lines.append("```") lines.append("[]...") lines.append("```") lines.append("") lines.append("- **Field codes** — single letter (a–z), case-insensitive") lines.append("- **Sub-IDs** — 1–4 lowercase alpha chars") lines.append("- **Ratings** — single octal digit (0–7), required on every sub-ID") lines.append("- **Custom sub-IDs** — `~` prefix: `~fermenting5`") lines.append("- **Modifiers** — after the digit:") lines.append(" - `$` — paid") lines.append(" - `+` followed by digit — aspiring toward that level") lines.append(" - `$+` combined — paid and aspiring") lines.append( '- **Alternatives** — `/` between two digits on direct-value fields: `c3/6`' ) lines.append("") lines.append("### Special formats") lines.append("") lines.append("| Field | Format | Example |") lines.append("|---|---|---|") lines.append( "| `g` geek type | `/` separates domains, `~` for custom, `$` for paid, no ratings | `gcs$/ai/wr~fermenting` |" ) lines.append("| `b` build | `/` | `b6/3` |") lines.append("| `l` languages | ISO 639-1 code + rating | `lar7en6` |") lines.append("| `v` politics | `/` | `v5/3` |") lines.append("") lines.append("### Parser algorithm") lines.append("") lines.append( "After each comma or space, read a single letter (field code). Then:" ) lines.append("") lines.append( "- `g` → read alpha tokens separated by `/` and `~`-prefixed tokens until delimiter" ) lines.append("- `b` → digit, `/`, digit") lines.append("- `v` → digit, `/`, digit") lines.append("- `l` → repeat: 2 alpha + digit + optional modifier") lines.append("- `r` → one sub-id + digit (single value only)") lines.append("- direct fields → digit + optional modifier") lines.append("- direct-alt fields → digit + optional `/` + digit + optional modifier") lines.append("- multi fields → repeat: alpha (sub-id) + digit + optional modifier") lines.append("") lines.append("---") lines.append("") return "\n".join(lines) def gen_scale(scale): lines = [] lines.append("## Rating scale (0–7)") lines.append("") lines.append("| Value | General | Proficiency | Enthusiasm | Stance |") lines.append("|---|---|---|---|---|") for v in scale["values"]: lines.append( f"| {v['value']} | {v['general']} | {v['proficiency']} | " f"{v['enthusiasm']} | {v['stance']} |" ) lines.append("") return "\n".join(lines) def gen_modifier_permissions(fields, modifiers): lines = [] lines.append("### Modifier permissions") lines.append("") lines.append("| Modifiers | Fields |") lines.append("|---|---|") both = [] paid_only = [] aspire_only = [] neither = [] for code in sorted(fields.keys()): f = fields[code] mods = f.get("modifiers", {}) has_paid = mods.get("paid", False) has_aspire = mods.get("aspire", False) if has_paid and has_aspire: both.append(code) elif has_paid: paid_only.append(code) elif has_aspire: aspire_only.append(code) else: neither.append(code) if both: lines.append( f"| `$` and `+` | {' '.join(f'`{c}`' for c in both)} |" ) if paid_only: lines.append( f"| `$` only | {' '.join(f'`{c}`' for c in paid_only)} |" ) if aspire_only: lines.append( f"| `+` only | {' '.join(f'`{c}`' for c in aspire_only)} |" ) if neither: lines.append( f"| Neither | {' '.join(f'`{c}`' for c in neither)} |" ) lines.append("") lines.append("---") lines.append("") return "\n".join(lines) def gen_field_list(fields, fmt): lines = [] lines.append("## Field list") lines.append("") lines.append("| Code | Field | Type | Category |") lines.append("|---|---|---|---|") reserved = fmt.get("reserved_codes", []) all_codes = sorted(set(list(fields.keys()) + reserved)) for code in all_codes: if code in reserved: lines.append(f"| `{code}` | *(reserved)* | — | — |") continue f = fields[code] ftype = f["type"] mods = f.get("modifiers", {}) has_alt = mods.get("alternative", False) type_str = ftype.capitalize() if ftype == "direct" and has_alt: type_str = "Direct, `/` ok" elif ftype == "special": if code == "b": type_str = "Special (h/w)" elif code == "v": type_str = "Special (s/e)" elif code == "g": type_str = "Special" elif code == "l": type_str = "Multi (ISO)" elif ftype == "multi": if code == "h": type_str = "Multi (1-letter)" elif code == "l": type_str = "Multi (ISO)" elif ftype == "single": type_str = "Single" lines.append( f"| `{code}` | {f['name']} | {type_str} | {f['category'].title()} |" ) lines.append("") lines.append("---") lines.append("") return "\n".join(lines) def gen_field_def_direct(code, f): """Generate a direct-type field definition.""" lines = [] vals = f.get("values", {}) if vals: for i in range(8): v = vals.get(str(i), {}) label = v.get("label", "—") humor = v.get("humor") if humor: lines.append(f"{i}. **{label}** — {humor}") else: lines.append(f"{i}. **{label}**") lines.append("") return "\n".join(lines) def gen_field_def_special_b(f): """Generate build field definition.""" lines = [] lines.append( "Format: `b/`. Each 0–7. " "0 = extremely small, 4 = average, 7 = extremely large. " "`+` modifier permitted." ) lines.append("") for dim_name, dim in f["dimensions"].items(): lines.append(f"**{dim_name.title()}:** ", ) vals = dim["values"] parts = [f"{k} = {v}" for k, v in sorted(vals.items(), key=lambda x: int(x[0]))] lines[-1] += " · ".join(parts) lines.append("") lines.append("> `b6/3` — tall and slim.") lines.append("> `b4/4` — average. Chairs were designed for you.") lines.append("") return "\n".join(lines) def gen_field_def_special_v(f): """Generate politics field definition.""" lines = [] lines.append("Format: `v/`. No modifiers.") lines.append("") for dim_name, dim in f["dimensions"].items(): vals = dim["values"] low = vals.get("0", "") mid = vals.get("4", "") high = vals.get("7", "") lines.append(f"**{dim_name.title()}:** 0 = {low} · 4 = {mid} · 7 = {high}") lines.append("") lines.append("> `v5/3` — socially center-left, economically center-right.") lines.append("> `v4/4` — centrist. You annoy both sides equally.") lines.append("") return "\n".join(lines) def gen_field_def_special_g(f): """Generate geek type field definition.""" lines = [] lines.append( "Mandatory. `/` separates domains. `~` prefixes custom domains. " "`$` after a domain = paid. No ratings." ) lines.append("") groups = f.get("sub_id_groups", {}) for group_key, group in groups.items(): label = group.get("label", group_key) sub_ids = group.get("sub_ids", {}) # For larger groups, use a table items = list(sub_ids.items()) if len(items) > 7: lines.append(f"#### {label}") lines.append("") lines.append("| Code | Domain | Code | Domain |") lines.append("|---|---|---|---|") # Pair them up for i in range(0, len(items), 2): c1, n1 = items[i] if isinstance(n1, dict): n1 = n1.get("name", n1.get("long", c1)) row = f"| `{c1}` | {n1}" if i + 1 < len(items): c2, n2 = items[i + 1] if isinstance(n2, dict): n2 = n2.get("name", n2.get("long", c2)) row += f" | `{c2}` | {n2} |" else: row += " | | |" lines.append(row) lines.append("") else: # Inline format for smaller groups lines.append(f"#### {label}") lines.append("") parts = [] for c, n in items: if isinstance(n, dict): n = n.get("name", n.get("long", c)) parts.append(f"`{c}` {n}") lines.append(" · ".join(parts)) lines.append("") lines.append("#### Custom") lines.append("") lines.append( "`~fermenting` `~beekeeping` `~lockpicking` `~origami` — anything goes." ) lines.append("") lines.append("> `gcs$/ai/wr~fermenting` — paid CS geek, also into AI, writing, and fermenting.") lines.append("") return "\n".join(lines) def gen_sub_id_table(sub_ids): """Generate a sub-ID table from a flat sub_ids dict.""" lines = [] items = list(sub_ids.items()) # Check if it's a two-column table or three-column has_long = any( isinstance(v, dict) and "long" in v for v in sub_ids.values() ) if has_long: # Pair them for a wider table lines.append("| Short | (Long) | Name | Short | (Long) | Name |") lines.append("|---|---|---|---|---|---|") for i in range(0, len(items), 2): c1, v1 = items[i] if isinstance(v1, dict): long1 = f"`({v1['long']})`" if "long" in v1 else "—" name1 = v1.get("name", c1) else: long1 = "—" name1 = v1 row = f"| `{c1}` | {long1} | {name1}" if i + 1 < len(items): c2, v2 = items[i + 1] if isinstance(v2, dict): long2 = f"`({v2['long']})`" if "long" in v2 else "—" name2 = v2.get("name", c2) else: long2 = "—" name2 = v2 row += f" | `{c2}` | {long2} | {name2} |" else: row += " | | | |" lines.append(row) else: lines.append("| Short | Name |") lines.append("|---|---|") for c, v in items: if isinstance(v, dict): name = v.get("name", c) else: name = v lines.append(f"| `{c}` | {name} |") lines.append("") return "\n".join(lines) def gen_grouped_sub_ids(groups, inline_threshold=7): """Generate sub-ID tables from grouped sub_ids.""" lines = [] for group_key, group in groups.items(): label = group.get("label", group_key) sub_ids = group.get("sub_ids", {}) items = list(sub_ids.items()) # Check for long forms has_long = any( isinstance(v, dict) and "long" in v for v in sub_ids.values() ) if len(items) <= inline_threshold and not has_long: # Inline format parts = [] for c, v in items: if isinstance(v, dict): long_str = f"({v['long']}) " if "long" in v else "" name = v.get("name", c) else: long_str = "" name = v parts.append(f"`{c}` {long_str}{name}") lines.append(f"**{label}:**") lines.append(" · ".join(parts)) lines.append("") else: # Table format with long forms lines.append(f"**{label}:**") lines.append("") if has_long: lines.append("| Short | (Long) | Name | Short | (Long) | Name |") lines.append("|---|---|---|---|---|---|") for i in range(0, len(items), 2): c1, v1 = items[i] if isinstance(v1, dict): long1 = f"`({v1['long']})`" if "long" in v1 else "—" name1 = v1.get("name", c1) else: long1 = "—" name1 = v1 row = f"| `{c1}` | {long1} | {name1}" if i + 1 < len(items): c2, v2 = items[i + 1] if isinstance(v2, dict): long2 = f"`({v2['long']})`" if "long" in v2 else "—" name2 = v2.get("name", c2) else: long2 = "—" name2 = v2 row += f" | `{c2}` | {long2} | {name2} |" else: row += " | | | |" lines.append(row) else: lines.append("| Short | Name |") lines.append("|---|---|") for c, v in items: if isinstance(v, dict): name = v.get("name", c) else: name = v lines.append(f"| `{c}` | {name} |") lines.append("") return "\n".join(lines) def gen_field_def_multi(code, f, scale): """Generate a multi-type field definition.""" lines = [] # Sub-IDs if "sub_id_groups" in f: lines.append(gen_grouped_sub_ids(f["sub_id_groups"])) elif "sub_ids" in f: lines.append(gen_sub_id_table(f["sub_ids"])) # Scale info scale_type = f.get("scale_type") if scale_type and scale_type != "custom": lines.append(f"Scale: **{scale_type}** (see global scale table).") lines.append("") # Custom scale labels if "scale_labels" in f: humor = f.get("humor_scale", {}) or {} for i in range(8): label = f["scale_labels"].get(str(i), "—") h = humor.get(str(i)) if h: lines.append(f"{i}. **{label}** — {h}") else: lines.append(f"{i}. **{label}**") lines.append("") elif f.get("humor_scale"): humor = f["humor_scale"] for val in ["0", "7"]: if val in humor: lines.append(f"> {val}: {humor[val]}") lines.append("") return "\n".join(lines) def gen_field_def_single(code, f): """Generate a single-type field definition.""" lines = [] lines.append("**Single value only** — one tradition + devoutness rating. Parser rejects multiple sub-IDs.") lines.append("") if "sub_ids" in f: lines.append(gen_sub_id_table(f["sub_ids"])) if "scale_labels" in f: sl = f["scale_labels"] lines.append(f"Rating = devoutness: 0 = {sl.get('0', '')}, 4 = {sl.get('4', '')}, 7 = {sl.get('7', '')}.") lines.append("") # Example if code == "r": lines.append("> `ris6` — devout Muslim. `rath4` — culturally atheist.") lines.append("") return "\n".join(lines) def gen_field_def_lang(f): """Generate spoken languages field definition.""" lines = [] lines.append("ISO 639-1 codes + proficiency 0–7. `$` and `+` permitted.") lines.append("") if "scale_labels" in f: humor = f.get("humor_scale", {}) or {} for i in range(8): label = f["scale_labels"].get(str(i), "—") h = humor.get(str(i)) if h: lines.append(f"{i}. **{label}** — {h}") else: lines.append(f"{i}. **{label}**") lines.append("") # Common codes codes = f.get("common_codes", {}) if codes: items = list(codes.items()) # Show first ~25 inline parts = [f"`{c}` {n}" for c, n in items[:26]] lines.append("Common codes: " + " · ".join(parts)) lines.append("") lines.append( "> `lar7$en6de3+5` — native Arabic (paid translator), fluent English, " "learning German (beginner, aspiring proficient)." ) lines.append("") return "\n".join(lines) def gen_field_definitions(fields, scale): lines = [] lines.append("## Field definitions") lines.append("") lines.append("---") lines.append("") for code in sorted(fields.keys()): f = fields[code] lines.append(f"### {code} — {f['name']}") lines.append("") ftype = f["type"] if ftype == "direct": lines.append(gen_field_def_direct(code, f)) elif ftype == "special": if code == "g": lines.append(gen_field_def_special_g(f)) elif code == "b": lines.append(gen_field_def_special_b(f)) elif code == "v": lines.append(gen_field_def_special_v(f)) elif code == "l": lines.append(gen_field_def_lang(f)) elif ftype == "multi": # Special intro for certain fields if code == "d": lines.append("The editor holy war, now quantified.") lines.append("") if code == "l": # l is type "multi" in JSON but uses ISO 639-1 codes lines.append(gen_field_def_lang(f)) else: lines.append(gen_field_def_multi(code, f, scale)) elif ftype == "single": lines.append(gen_field_def_single(code, f)) # Note modifiers mods = f.get("modifiers", {}) mod_parts = [] if mods.get("paid"): mod_parts.append("`$` paid") if mods.get("aspire"): mod_parts.append("`+` aspire") if mods.get("alternative"): mod_parts.append("`/` alternative") # Only show if there are interesting modifiers to mention # (skip if already covered in special format text) if mod_parts and ftype not in ("special",): pass # Modifiers are covered in the permissions table lines.append("---") lines.append("") return "\n".join(lines) def gen_examples(examples, fields): lines = [] lines.append("## Examples") lines.append("") for ex in examples: handle = ex["handle"] desc = ex["description"] uri = ex["uri"] lines.append(f"### {handle} — {desc}") lines.append("") # Generate block format from URI block = uri_to_block(uri, fields) lines.append("```") for bl in block: lines.append(bl) lines.append("```") lines.append("") lines.append("```") lines.append(uri) lines.append("```") lines.append("") lines.append("---") lines.append("") return "\n".join(lines) def uri_to_block(uri, fields): """Convert a URI string to block format lines.""" # Parse: ugi:@: # Just use the existing block examples from the hand-written spec # since exact conversion logic is complex. For now, output the URI. # Actually, let's parse it properly. rest = uri if rest.startswith("ugi:"): rest = rest[4:] # Split version@handle:fields at_idx = rest.index("@") version = rest[:at_idx] rest = rest[at_idx + 1:] colon_idx = rest.index(":") handle = rest[:colon_idx] field_str = rest[colon_idx + 1:] field_parts = field_str.split(",") block_lines = [] block_lines.append("------- BEGIN UGI BLOCK -------") # First line: version, handle, and g field first_line = f"v:{version} @{handle}" remaining = [] for part in field_parts: if not part: continue code = part[0].lower() if code == "g": # Geek type goes on first line first_line += f" G{part[1:]}" else: remaining.append(part) block_lines.append(first_line) # Group remaining fields into lines by category identity = [] tech = [] stance = [] entertainment = [] lifestyle = [] for part in remaining: code = part[0].lower() f = fields.get(code) if not f: tech.append(part) continue cat = f.get("category", "") # Expand multi fields expanded = expand_field_block(part, f) if cat == "identity": identity.extend(expanded) elif cat in ("tech",): tech.extend(expanded) elif cat == "stance": stance.extend(expanded) elif cat == "entertainment": entertainment.extend(expanded) elif cat in ("lifestyle", "appearance"): lifestyle.extend(expanded) else: tech.extend(expanded) # Output lines if identity: block_lines.append(" ".join(identity)) if tech: block_lines.append(" ".join(tech)) if stance: block_lines.append(" ".join(stance)) if entertainment: block_lines.append(" ".join(entertainment)) if lifestyle: block_lines.append(" ".join(lifestyle)) block_lines.append("-------- END UGI BLOCK --------") return block_lines def expand_field_block(part, f): """Expand a URI field part into block format parts.""" code = part[0] rest = part[1:] ftype = f["type"] uc = code.upper() if ftype == "direct": return [f"{uc}{rest}"] elif ftype == "special": if code == "b" or code == "v": return [f"{uc}{rest}"] elif code == "g": return [f"G{rest}"] elif code == "l": # Parse: 2-char code + digit + mods, repeating expanded = [] i = 0 while i < len(rest): if rest[i] == "~": # custom j = i + 1 while j < len(rest) and rest[j].isalpha(): j += 1 sub = rest[i:j] digit = rest[j] if j < len(rest) and rest[j].isdigit() else "" mod = "" k = j + 1 while k < len(rest) and rest[k] in "$+0123456789": mod += rest[k] k += 1 expanded.append(f"L{sub}{digit}{mod}") i = k elif rest[i].isalpha(): sub = rest[i:i + 2] digit = rest[i + 2] if i + 2 < len(rest) else "" mod = "" k = i + 3 while k < len(rest) and rest[k] in "$+0123456789": mod += rest[k] k += 1 expanded.append(f"L{sub}{digit}{mod}") i = k else: i += 1 return expanded return [f"{uc}{rest}"] elif ftype in ("multi", "single"): # Parse sub-id + digit + mods expanded = [] i = 0 while i < len(rest): if rest[i] == "~": j = i + 1 while j < len(rest) and rest[j].isalpha(): j += 1 sub = rest[i:j] digit = rest[j] if j < len(rest) and rest[j].isdigit() else "" mod = "" k = j + 1 while k < len(rest) and rest[k] in "$+0123456789": mod += rest[k] k += 1 expanded.append(f"{uc}{sub}{digit}{mod}") i = k elif rest[i].isalpha(): # Read sub-id (1-4 alpha chars) j = i while j < len(rest) and rest[j].isalpha(): j += 1 sub = rest[i:j] digit = rest[j] if j < len(rest) and rest[j].isdigit() else "" mod = "" k = j + 1 while k < len(rest) and rest[k] in "$+0123456789": mod += rest[k] k += 1 expanded.append(f"{uc}{sub}{digit}{mod}") i = k else: i += 1 return expanded return [f"{uc}{rest}"] def gen_extending(): lines = [] lines.append("## Extending UGI") lines.append("") lines.append("Any field with sub-IDs accepts custom entries via `~`:") lines.append("") lines.append("```") lines.append("gcs/~mycology geek of CS and mycology") lines.append("m~synthwave6 synthwave enthusiast") lines.append("t~severance7 severance obsessed") lines.append("j~larp5 LARP, competent") lines.append("o~beos4 BeOS, neutral nostalgia") lines.append("```") lines.append("") lines.append("Letters `n` and `u` are reserved for future spec versions.") lines.append("") lines.append("---") lines.append("") return "\n".join(lines) def gen_comparison(comparison): lines = [] lines.append("## Comparison with predecessors") lines.append("") lines.append( "| | Geek Code 3.12 (1996) | Geek Code 2026 | Hacker Key v4 (2006) | UGI v0 |" ) lines.append("|---|---|---|---|---|") for row in comparison["features"]: lines.append("| " + " | ".join(row) + " |") lines.append("") # Retained lines.append("### Fields retained") lines.append("") parts = [] for r in comparison["retained"]: parts.append(f"`{r['ugi']}` {r['note'].lower()}") lines.append( ", ".join(parts) + " — all present in one or both predecessors." ) lines.append("") # Added lines.append("### Fields added") lines.append("") for a in comparison["added"]: lines.append(f"`{a['ugi']}` — {a['reason']}. ", ) lines.append("") # Removed lines.append("### Fields removed") lines.append("") parts = [] for r in comparison["removed"]: parts.append(f"{r['field']} — {r['reason'].lower()}") lines.append(". ".join(parts) + ".") lines.append("") lines.append("---") lines.append("") return "\n".join(lines) def gen_quick_ref(fields): lines = [] lines.append("## Quick reference") lines.append("") lines.append("```") lines.append("FORMAT: ugi:[/]@:") lines.append( "SCALE: 0=hostile 1=dislike 2=meh 3=slight- 4=neutral 5=like 6=strong 7=obsessed" ) lines.append( "MODIFY: $ = paid + = aspire / = fluctuate,separate ~ = custom" ) lines.append("") # Field codes codes = sorted(fields.keys()) all_codes = [] for c in "abcdefghijklmnopqrstuvwxyz": if c in fields: f = fields[c] name = f["name"].lower() all_codes.append(f"{c} {name}") elif c in ("n", "u"): all_codes.append(f"[{c} reserved]") # Print in rows of 5 for i in range(0, len(all_codes), 5): row = all_codes[i : i + 5] lines.append(" ".join(f"{x:<14}" for x in row).rstrip()) lines.append("") # Modifier groups both = [] paid_only = [] aspire_only = [] neither = [] for code in sorted(fields.keys()): f = fields[code] mods = f.get("modifiers", {}) has_paid = mods.get("paid", False) has_aspire = mods.get("aspire", False) if has_paid and has_aspire: both.append(code) elif has_paid: paid_only.append(code) elif has_aspire: aspire_only.append(code) else: neither.append(code) lines.append(f"$+ {' '.join(both)} $ only {' '.join(paid_only)} + only {' '.join(aspire_only)} none {' '.join(neither)}") lines.append("```") lines.append("") lines.append("---") lines.append("") return "\n".join(lines) def gen_references(spec): lines = [] lines.append("## References") lines.append("") for p in spec["predecessors"]: lines.append(f"- [{p['name']} (v0.1–latest)]({p['url']}) — {p['author']}, {p['years']}") if "continuation" in p: c = p["continuation"] lines.append(f"- [{c['name']}]({c['url']}) — {c['author']}, {c['years']}") for r in spec.get("references", []): lines.append(f"- [{r['title']}]({r['url']})") lines.append("") lines.append("---") lines.append("") lines.append("## License") lines.append("") lines.append(f"{spec['license']} — see [LICENSE](./LICENSE).") lines.append("") return "\n".join(lines) def main(): if len(sys.argv) != 3: print(f"Usage: {sys.argv[0]} ", file=sys.stderr) sys.exit(1) registry_path, output_path = sys.argv[1], sys.argv[2] reg = load_registry(registry_path) spec = reg["spec"] fmt = reg["format"] scale = reg["scale"] fields = reg["fields"] modifiers = reg["modifiers"] examples = reg["examples"] comparison = reg["comparison"] parts = [ gen_title(spec), gen_overview(spec), gen_formats(fmt), gen_grammar(fields), gen_scale(scale), gen_modifier_permissions(fields, modifiers), gen_field_list(fields, fmt), gen_field_definitions(fields, scale), gen_extending(), gen_examples(examples, fields), gen_comparison(comparison), gen_quick_ref(fields), gen_references(spec), ] # Each part ends with "\n" from join; ensure blank line between sections output = "\n".join(p.rstrip("\n") for p in parts) + "\n" os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) with open(output_path, "w") as f: f.write(output) print(f"Generated {output_path}") if __name__ == "__main__": main()