From 41f8d3025f041aaab98fcc3511cb6ec2198f76a7 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sun, 14 Jun 2026 01:49:48 +0300 Subject: init: vibed --- spec/scripts/gen_abnf.py | 265 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 spec/scripts/gen_abnf.py (limited to 'spec/scripts/gen_abnf.py') diff --git a/spec/scripts/gen_abnf.py b/spec/scripts/gen_abnf.py new file mode 100644 index 0000000..9569195 --- /dev/null +++ b/spec/scripts/gen_abnf.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +"""Generate UGI_GRAMMAR_v0.abnf 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 hex_char(c): + """Return ABNF hex notation for a character.""" + return f"%x{ord(c):02X}" + + +def hex_range(start, end): + """Return ABNF hex range.""" + return f"%x{ord(start):02X}-{ord(end):02X}" + + +def collect_sub_ids(f): + """Collect all sub-ID codes from a field.""" + ids = [] + if "sub_ids" in f: + ids.extend(f["sub_ids"].keys()) + if "sub_id_groups" in f: + for group in f["sub_id_groups"].values(): + if "sub_ids" in group: + ids.extend(group["sub_ids"].keys()) + return ids + + +def gen_sub_id_rule(name, sub_ids): + """Generate ABNF rule matching any of the given sub-ID strings.""" + # Quote each as case-insensitive string + parts = [] + for sid in sorted(set(sub_ids)): + # ABNF quoted strings are case-insensitive by default + parts.append(f'"{sid}"') + return f"{name} = " + " / ".join(parts) + + +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) + fields = reg["fields"] + + lines = [] + lines.append("; UGI Grammar v0 — RFC 5234 ABNF") + lines.append("; Generated from ugi_registry_v0.json") + lines.append(";") + lines.append("; Compatible with abnf.dev/abnf2svg") + lines.append("") + + # ── Top-level ── + lines.append("; === Top-level ===") + lines.append("") + lines.append('ugi-string = "ugi:" version "@" handle ":" field-list') + lines.append("") + lines.append('version = 1*DIGIT ["/" 1*DIGIT]') + lines.append("") + lines.append("; handle: alphanumeric, hyphens, underscores, dots") + lines.append("handle = 1*(ALPHA / DIGIT / %x2D / %x5F / %x2E)") + lines.append("") + lines.append('field-list = field *("," field)') + lines.append("") + + # ── Field dispatch ── + lines.append("; === Field dispatch ===") + lines.append("") + + # Build field alternatives + field_rules = [] + for code in sorted(fields.keys()): + field_rules.append(f"field-{code}") + + lines.append("field = " + " / ".join(field_rules)) + lines.append("") + + # ── Common rules ── + lines.append("; === Common rules ===") + lines.append("") + lines.append("ODIGIT = %x30-37") + lines.append("") + lines.append('paid = "$"') + lines.append("") + lines.append('aspire = "+" ODIGIT') + lines.append("") + lines.append("paid-aspire = paid [aspire]") + lines.append("") + lines.append("modifier = paid-aspire / aspire") + lines.append("") + lines.append('alternative = "/" ODIGIT') + lines.append("") + lines.append('; custom sub-ID: ~ followed by 2+ alpha chars') + lines.append('custom-sub = "~" 2*ALPHA') + lines.append("") + lines.append("; sub-ID: 1-4 alpha chars") + lines.append("sub-id = 1*4ALPHA") + lines.append("") + + # ── Per-field rules ── + lines.append("; === Per-field rules ===") + lines.append("") + + for code in sorted(fields.keys()): + f = fields[code] + ftype = f["type"] + mods = f.get("modifiers", {}) + has_paid = mods.get("paid", False) + has_aspire = mods.get("aspire", False) + has_alt = mods.get("alternative", False) + + hex_code = hex_char(code) + lines.append(f"; {code} — {f['name']} ({ftype})") + + if ftype == "direct": + # Build modifier part + mod_parts = [] + if has_alt: + mod_parts.append("alternative") + if has_paid and has_aspire: + mod_parts.append("modifier") + elif has_paid: + mod_parts.append("paid") + elif has_aspire: + mod_parts.append("aspire") + + if mod_parts: + mod_str = " [" + " / ".join(mod_parts) + "]" + else: + mod_str = "" + + lines.append(f"field-{code} = {hex_code} ODIGIT{mod_str}") + + elif ftype == "multi": + # Entry: sub-id + digit + optional modifier + mod_parts = [] + if has_paid and has_aspire: + mod_parts.append("modifier") + elif has_paid: + mod_parts.append("paid") + elif has_aspire: + mod_parts.append("aspire") + + if mod_parts: + mod_str = " [" + " / ".join(mod_parts) + "]" + else: + mod_str = "" + + entry_rule = f"field-{code}-entry" + if code == "l": + # l uses 2-char ISO 639-1 codes instead of generic sub-ids + lines.append( + f"{entry_rule} = 2ALPHA ODIGIT{mod_str}" + ) + else: + lines.append( + f"{entry_rule} = (sub-id / custom-sub) ODIGIT{mod_str}" + ) + lines.append( + f"field-{code} = {hex_code} 1*{entry_rule}" + ) + + elif ftype == "single": + mod_parts = [] + if has_paid and has_aspire: + mod_parts.append("modifier") + elif has_paid: + mod_parts.append("paid") + elif has_aspire: + mod_parts.append("aspire") + + if mod_parts: + mod_str = " [" + " / ".join(mod_parts) + "]" + else: + mod_str = "" + + lines.append( + f"field-{code} = {hex_code} (sub-id / custom-sub) ODIGIT{mod_str}" + ) + + elif ftype == "special": + if code == "g": + # g: domains separated by /, $ after domain, ~ for custom, no digits + lines.append(f'g-domain = 2*4ALPHA ["$"]') + lines.append(f'g-custom = "~" 2*ALPHA') + lines.append( + f'field-g = {hex_code} (g-domain / g-custom) *("/" (g-domain / g-custom))' + ) + elif code == "b": + # b: digit/digit, optional aspire on each + aspire_str = " [aspire]" if has_aspire else "" + lines.append( + f'field-{code} = {hex_code} ODIGIT{aspire_str} "/" ODIGIT{aspire_str}' + ) + elif code == "v": + lines.append( + f'field-{code} = {hex_code} ODIGIT "/" ODIGIT' + ) + else: + # Fallback + lines.append(f"field-{code} = {hex_code} 1*(ALPHA / DIGIT / %x24 / %x2B / %x2F / %x7E)") + + lines.append("") + + # ── Block format ── + lines.append("; === Block format ===") + lines.append("") + lines.append( + 'ugi-block = block-header CRLF block-first-line CRLF ' + '1*(block-field-line CRLF) block-footer' + ) + lines.append("") + lines.append('block-header = "------- BEGIN UGI BLOCK -------"') + lines.append('block-footer = "-------- END UGI BLOCK --------"') + lines.append("") + lines.append( + 'block-first-line = "v:" version SP "@" handle [SP block-geek-field]' + ) + lines.append("") + lines.append('; In block format, G field uses uppercase and appears on first line') + lines.append( + 'block-geek-field = "G" (g-domain / g-custom) *("/" (g-domain / g-custom))' + ) + lines.append("") + lines.append( + 'block-field-line = block-field *(SP block-field)' + ) + lines.append("") + lines.append( + "; Block fields use uppercase code prefix repeated per sub-ID" + ) + lines.append( + 'block-field = ALPHA 1*(ALPHA / DIGIT / %x24 / %x2B / %x2F / %x7E)' + ) + lines.append("") + + # ── Core rules ── + lines.append("; === Core rules (RFC 5234) ===") + lines.append("") + lines.append("ALPHA = %x41-5A / %x61-7A") + lines.append("DIGIT = %x30-39") + lines.append("SP = %x20") + lines.append("CRLF = %x0D.0A / %x0A") + lines.append("") + + output = "\n".join(lines) + + 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() -- cgit v1.2.3