#!/usr/bin/env python3 """Generate multi-page UGI web tool from ugi_registry_v0.json. Usage: gen_tool.py Produces: index.html, specs/index.html, encoder/index.html, decoder/index.html, converter/index.html """ import json import os import sys import html as htmlmod STYLE = """\ @font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Regular.woff2); font-weight: normal; } @font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Bold.woff2); font-weight: bold; } * { unicode-bidi: plaintext; box-sizing: border-box; } html { color: black; background-color: white; } body { font-family: "Kawkab Mono"; font-size: 16px; line-height: 1.4; margin: 0; padding: 4rem 0; min-height: 100%; overflow-wrap: break-word; } main, header, footer { max-width: 800px; margin-inline: auto; padding: 0 2rem; } h1, footer { text-align: center; } main { text-align: justify; } nav { text-align: start; } p, h2, h3, h4 { margin: 1em 0 0 0; } hr { border: none; border-top: thin solid; margin: 1.25rem 0; } header { margin-bottom: 1em; } footer { margin-top: 3em; } nav.subnav { margin: 0.5em 0 1.5em; } table { margin: 0; border-collapse: collapse; width: 100%; } th, td { border: 1px solid; padding: 0.3em 0.6em; text-align: left; } th { background: rgba(128,128,128,0.08); } pre { margin: 1em 0; } pre code { border: thin solid; padding: 1em; display: block; text-align: start; overflow-x: scroll; } code { font-size: 85%; } label { display: block; margin: 0.3rem 0 0.1rem; font-weight: bold; } input, select, textarea { font-family: inherit; font-size: inherit; border: 1px solid; padding: 0.3rem 0.5rem; } input[type="range"] { background: none; border: none; padding: 0; width: 200px; vertical-align: middle; } input[type="checkbox"] { width: auto; margin-right: 0.3rem; vertical-align: middle; } textarea { width: 100%; min-height: 100px; resize: vertical; } button { font-family: inherit; font-size: inherit; padding: 0.3rem 0.8rem; cursor: pointer; background: black; color: white; border: none; } button:hover { opacity: 0.7; } .field-section { margin: 0.5rem 0; padding: 0.5rem; border: 1px solid; } .field-section summary { cursor: pointer; font-weight: bold; } .chip { display: inline-block; padding: 0.15rem 0.5rem; margin: 0.15rem; background: rgba(128,128,128,0.15); font-size: 0.85rem; } .chip .remove { cursor: pointer; margin-left: 0.3rem; } .output-box { background: rgba(128,128,128,0.08); padding: 0.8rem; word-break: break-all; margin: 0.5rem 0; min-height: 2rem; border: 1px solid; } .decode-table { width: 100%; border-collapse: collapse; margin: 0.5rem 0; } .decode-table th, .decode-table td { text-align: left; padding: 0.3rem 0.5rem; border: 1px solid; } .decode-table th { background: rgba(128,128,128,0.1); } .humor { font-style: italic; font-size: 0.85rem; } .slider-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; } .slider-val { min-width: 1.5rem; text-align: center; font-weight: bold; } .slider-label { font-size: 0.85rem; } .multi-add { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin: 0.3rem 0; } details { margin: 0.5rem 0; } details summary { cursor: pointer; } .skip-check { margin-left: 0.5rem; font-weight: normal; font-size: 0.85rem; } @media (max-width: 600px) { body { font-size: 0.9em; } h1 { font-size: 1.8em; } } @media (max-width: 400px) { body { font-size: 0.8em; } h1 { font-size: 1.6em; } } @media (prefers-color-scheme: dark) { html { filter: invert(1); } img { filter: invert(1); } }""" FOOTER = """\ """ PAGES = [ ("about", "/", "about"), ("specs", "/specs/", "specs"), ("encoder", "/encoder/", "encoder"), ("decoder", "/decoder/", "decoder"), ("converter", "/converter/", "converter"), ] def subnav(active): parts = [] for key, href, label in PAGES: if key == active: parts.append(f"{label}") else: parts.append(f'{label}') return '" def page(title, h1, active, content, extra_style=""): style = STYLE + ("\n" + extra_style if extra_style else "") return f""" {title}

{h1}

{subnav(active)} {content}
{FOOTER} """ def build_home(reg): ver = reg["spec"]["version"] desc = htmlmod.escape(reg["spec"]["description"]) examples = """
ugi:0@jdoe:gcs$/dev,a4,b5/4,c3,hh4f5m4,e4,len7,oxa6we5,ppy6$js6$ts5rs4+6,dvs6,w5,q4,i5,v5/4,rag4,tst5ex7,k5,xxk6,jst5pc6,mrk5in4,s6,y4,f4,z3
ugi:0@neo:gcy$/sec/os,a3,b5/3,c1,hh2,e6,len6zh4,oxd7$fb6,pc6$ba7as5rs5,ded7,w6,q7$,i1,v6/5,rat5,tff7bs6bl6,k7,jro6pc7,mmt6el5,s2,y5,f3,z6
""" fields_summary = "\n".join( f"
  • {code} — {f['name']}
  • " for code, f in sorted(reg["fields"].items()) ) content = f"""

    {desc}

    design goals

    formats

    URI format:

    ugi:<version>[/<revision>]@<handle>:<field>,<field>,...

    Block format:

    ------- BEGIN UGI BLOCK -------
    v:<version> @<handle> G<geek_types>
    <field> <field> ...
    -------- END UGI BLOCK --------

    examples

    {examples}

    fields

    See the specs page for full field definitions.

    """ return page(f"ugi v{ver}", "ugi", "about", content) def build_specs(reg): ver = reg["spec"]["version"] out = "" out += "

    global scale (0–7)

    " out += '' for sv in reg["scale"]["values"]: out += f'' out += "
    #generalproficiencyenthusiasmstance
    {sv["value"]}{sv["general"]}{sv["proficiency"]}{sv["enthusiasm"]}{sv["stance"]}
    " all_codes = sorted(reg["fields"].keys()) codes = ["g"] + [c for c in all_codes if c != "g"] for code in codes: f = reg["fields"][code] req = " (required)" if code == "g" else "" out += f"

    {htmlmod.escape(code)} — {htmlmod.escape(f['name'])}{req}

    " if f.get("description"): out += f'

    {htmlmod.escape(f["description"])}

    ' meta = f'type: {f["type"]}' if f.get("category"): meta += f' | category: {f["category"]}' if f.get("scale_type"): meta += f' | scale: {f["scale_type"]}' mods = list(f.get("modifiers", {}).keys()) if mods: meta += " | modifiers: " + ", ".join(mods) out += f"

    {meta}

    " if f.get("values"): out += '
      ' for i in range(8): v = f["values"].get(str(i), {}) label = htmlmod.escape(v.get("label", "—")) humor = v.get("humor", "") out += f"
    1. {label}" if humor: out += f" — {htmlmod.escape(humor)}" out += "
    2. " out += "
    " if f.get("scale_labels"): out += '
      ' for i in range(8): label = htmlmod.escape(f["scale_labels"].get(str(i), "—")) humor = f.get("humor_scale", {}).get(str(i), "") out += f"
    1. {label}" if humor: out += f" — {htmlmod.escape(humor)}" out += "
    2. " out += "
    " elif f.get("humor_scale"): out += "

    notable:

    " if f.get("sub_ids"): out += "" if f.get("sub_id_groups"): for gkey, group in f["sub_id_groups"].items(): label = group.get("label", gkey) out += f"

    {htmlmod.escape(label)}

    " if f.get("dimensions"): for dimname, dim in f["dimensions"].items(): out += f"

    {htmlmod.escape(dimname)}

    " return page(f"ugi v{ver} / specs", "specs", "specs", out) def build_encoder(reg): ver = reg["spec"]["version"] reg_json = json.dumps(reg, ensure_ascii=False) content = f"""

    Build a UGI string field by field. The g (geek specialization) field is required.


    URI output

    block output

    
    
    """
    
        return page(f"ugi v{ver} / encoder", "encoder", "encoder", content)
    
    
    def build_decoder(reg):
        ver = reg["spec"]["version"]
        reg_json = json.dumps(reg, ensure_ascii=False)
    
        content = f"""

    Paste a UGI string (URI or block format) to decode it.

    """ return page(f"ugi v{ver} / decoder", "decoder", "decoder", content) def build_converter(reg): ver = reg["spec"]["version"] reg_json = json.dumps(reg, ensure_ascii=False) content = f"""

    Convert between URI and block formats.

    converted output

    
    
    """
    
        return page(f"ugi v{ver} / converter", "converter", "converter", content)
    
    
    def _shared_js():
        return r"""
    function copyOutput(id) {
      const text = document.getElementById(id).textContent;
      navigator.clipboard.writeText(text);
    }
    
    function getScale(field) {
      const f = REG.fields[field];
      if (!f) return {};
      if (f.values) return f.values;
      if (f.scale_labels) {
        const out = {};
        for (const [k,v] of Object.entries(f.scale_labels)) out[k] = {label: v};
        return out;
      }
      const st = f.scale_type;
      if (st && st !== 'custom') {
        const out = {};
        for (const v of REG.scale.values) out[v.value] = {label: v[st] || v.general};
        return out;
      }
      return {};
    }
    
    function getSubIds(field) {
      const f = REG.fields[field];
      if (!f) return [];
      const out = [];
      if (f.sub_ids) {
        for (const [k,v] of Object.entries(f.sub_ids)) {
          const name = typeof v === 'object' ? (v.name || k) : v;
          out.push({code: k, name});
        }
      }
      if (f.sub_id_groups) {
        for (const [gk, group] of Object.entries(f.sub_id_groups)) {
          const label = group.label || gk;
          for (const [k,v] of Object.entries(group.sub_ids || {})) {
            const name = typeof v === 'object' ? (v.name || k) : v;
            out.push({code: k, name, group: label});
          }
        }
      }
      if (field === 'l' && f.common_codes) {
        for (const [k,v] of Object.entries(f.common_codes)) out.push({code: k, name: v});
      }
      return out;
    }
    
    function getGeekDomains() {
      const f = REG.fields.g;
      const out = [];
      if (f.sub_id_groups) {
        for (const [gk, group] of Object.entries(f.sub_id_groups)) {
          const label = group.label || gk;
          for (const [k,v] of Object.entries(group.sub_ids || {})) {
            const name = typeof v === 'string' ? v : (v.name || k);
            out.push({code: k, name, group: label});
          }
        }
      }
      return out;
    }
    
    function resolveSubId(code, sub, f) {
      sub = sub.toLowerCase();
      if (f.sub_ids && f.sub_ids[sub]) {
        const v = f.sub_ids[sub];
        return typeof v === 'object' ? (v.name || sub) : v;
      }
      if (f.sub_id_groups) {
        for (const group of Object.values(f.sub_id_groups)) {
          if (group.sub_ids && group.sub_ids[sub]) {
            const v = group.sub_ids[sub];
            return typeof v === 'object' ? (v.name || sub) : v;
          }
        }
      }
      return sub;
    }
    
    function resolveGeekDomain(code) {
      code = code.toLowerCase();
      const f = REG.fields.g;
      if (f.sub_id_groups) {
        for (const group of Object.values(f.sub_id_groups)) {
          if (group.sub_ids && group.sub_ids[code]) {
            const v = group.sub_ids[code];
            return typeof v === 'string' ? v : (v.name || code);
          }
        }
      }
      return code;
    }
    
    function modStr(mod) {
      let s = '';
      if (mod.includes('$')) s += ' [paid]';
      const m = mod.match(/\+(\d)/);
      if (m) s += ' [aspiring to ' + m[1] + ']';
      return s;
    }
    """
    
    
    def _encoder_js():
        return _shared_js() + r"""
    const encoderState = {};
    
    function markIncluded(code) {
      const el = document.getElementById('skip-' + code);
      if (el && !el.disabled) el.checked = true;
    }
    
    function buildEncoder() {
      const container = document.getElementById('enc-fields');
      const allCodes = Object.keys(REG.fields).sort();
      const codes = ['g', ...allCodes.filter(c => c !== 'g')];
    
      for (const code of codes) {
        const f = REG.fields[code];
        const isRequired = code === 'g';
        const section = document.createElement('details');
        section.className = 'field-section';
        if (isRequired) section.open = true;
    
        const summary = document.createElement('summary');
        const skipId = 'skip-' + code;
        const reqLabel = isRequired ? ' (required)' : '';
        summary.innerHTML = code.toUpperCase() + ' — ' + f.name + reqLabel +
          '';
        section.appendChild(summary);
    
        const inner = document.createElement('div');
        inner.id = 'enc-field-' + code;
    
        if (f.type === 'direct') buildDirectField(inner, code, f);
        else if (f.type === 'multi') buildMultiField(inner, code, f);
        else if (f.type === 'single') buildSingleField(inner, code, f);
        else if (f.type === 'special') {
          if (code === 'g') buildGeekField(inner, f);
          else if (code === 'b') buildBuildField(inner, f);
          else if (code === 'v') buildPoliticsField(inner, f);
          else if (code === 'l') buildLangField(inner, f);
        }
    
        section.appendChild(inner);
        container.appendChild(section);
        if (code !== 'g') document.getElementById(skipId).checked = false;
      }
    }
    
    function buildDirectField(container, code, f) {
      const scale = getScale(code);
      const mods = f.modifiers || {};
      const row = document.createElement('div');
      row.className = 'slider-row';
      const slider = document.createElement('input');
      slider.type = 'range'; slider.min = 0; slider.max = 7; slider.value = 4;
      slider.id = 'enc-' + code + '-val';
      slider.oninput = function() {
        document.getElementById('enc-' + code + '-display').textContent = this.value;
        const s = scale[this.value];
        document.getElementById('enc-' + code + '-label').textContent = s ? (s.label || '') : '';
        const h = s ? (s.humor || '') : '';
        document.getElementById('enc-' + code + '-humor').textContent = h;
        markIncluded(code); encodeUGI();
      };
      row.appendChild(slider);
      const val = document.createElement('span');
      val.className = 'slider-val'; val.id = 'enc-' + code + '-display'; val.textContent = '4';
      row.appendChild(val);
      const lab = document.createElement('span');
      lab.className = 'slider-label'; lab.id = 'enc-' + code + '-label';
      lab.textContent = scale['4'] ? (scale['4'].label || '') : '';
      row.appendChild(lab);
      container.appendChild(row);
      const humor = document.createElement('div');
      humor.className = 'humor'; humor.id = 'enc-' + code + '-humor';
      humor.textContent = scale['4'] ? (scale['4'].humor || '') : '';
      container.appendChild(humor);
      if (mods.alternative) {
        const altDiv = document.createElement('div');
        altDiv.innerHTML = '';
        const altSlider = document.createElement('input');
        altSlider.type = 'range'; altSlider.min = 0; altSlider.max = 7; altSlider.value = 4;
        altSlider.id = 'enc-' + code + '-alt'; altSlider.oninput = function() { encodeUGI(); };
        altDiv.appendChild(altSlider); container.appendChild(altDiv);
      }
      if (mods.paid) {
        const pDiv = document.createElement('div');
        pDiv.innerHTML = '';
        container.appendChild(pDiv);
      }
      if (mods.aspire) {
        const aDiv = document.createElement('div');
        aDiv.innerHTML = '';
        const aSel = document.createElement('select');
        aSel.id = 'enc-' + code + '-aspire';
        for (let i = 5; i <= 7; i++) {
          const opt = document.createElement('option');
          opt.value = i; opt.textContent = i + ' — ' + (scale[i] ? (scale[i].label || '') : '');
          aSel.appendChild(opt);
        }
        aSel.onchange = function() { encodeUGI(); };
        aDiv.appendChild(aSel); container.appendChild(aDiv);
      }
    }
    
    function buildMultiField(container, code, f) {
      const subIds = getSubIds(code);
      const mods = f.modifiers || {};
      const scale = getScale(code);
      encoderState[code] = [];
      const addRow = document.createElement('div');
      addRow.className = 'multi-add';
      const sel = document.createElement('select');
      sel.id = 'enc-' + code + '-sel';
      const defOpt = document.createElement('option');
      defOpt.value = ''; defOpt.textContent = '— select —';
      sel.appendChild(defOpt);
      let lastGroup = '';
      for (const s of subIds) {
        if (s.group && s.group !== lastGroup) {
          const optg = document.createElement('optgroup');
          optg.label = s.group; sel.appendChild(optg); lastGroup = s.group;
        }
        const opt = document.createElement('option');
        opt.value = s.code; opt.textContent = s.code + ' — ' + s.name;
        if (lastGroup) sel.lastElementChild.appendChild(opt);
        else sel.appendChild(opt);
      }
      addRow.appendChild(sel);
      const rSlider = document.createElement('input');
      rSlider.type = 'range'; rSlider.min = 0; rSlider.max = 7; rSlider.value = 5;
      rSlider.id = 'enc-' + code + '-rating';
      addRow.appendChild(rSlider);
      const rVal = document.createElement('span');
      rVal.className = 'slider-val'; rVal.id = 'enc-' + code + '-rating-display'; rVal.textContent = '5';
      addRow.appendChild(rVal);
      const rLabel = document.createElement('span');
      rLabel.className = 'slider-label'; rLabel.id = 'enc-' + code + '-rating-label';
      rLabel.textContent = scale['5'] ? (scale['5'].label || '') : '';
      rSlider.oninput = function() {
        rVal.textContent = this.value;
        const s = scale[this.value]; rLabel.textContent = s ? (s.label || '') : '';
      };
      addRow.appendChild(rLabel);
      if (mods.paid) {
        const pCb = document.createElement('label');
        pCb.innerHTML = ' $';
        addRow.appendChild(pCb);
      }
      if (mods.aspire) {
        const aCb = document.createElement('label');
        aCb.innerHTML = ' +';
        addRow.appendChild(aCb);
        const aSel = document.createElement('select');
        aSel.id = 'enc-' + code + '-add-aspire';
        for (let i = 5; i <= 7; i++) {
          const opt = document.createElement('option');
          opt.value = i; opt.textContent = i + ' — ' + (scale[i] ? (scale[i].label || '') : '');
          aSel.appendChild(opt);
        }
        addRow.appendChild(aSel);
      }
      const addBtn = document.createElement('button');
      addBtn.textContent = 'add';
      addBtn.onclick = function() {
        const subId = sel.value; if (!subId) return;
        const entry = {sub: subId, rating: parseInt(rSlider.value)};
        if (mods.paid) { const pEl = document.getElementById('enc-' + code + '-add-paid'); if (pEl && pEl.checked) entry.paid = true; }
        if (mods.aspire) {
          const aOn = document.getElementById('enc-' + code + '-add-aspire-on');
          const aVal = document.getElementById('enc-' + code + '-add-aspire');
          if (aOn && aOn.checked) entry.aspire = parseInt(aVal.value);
        }
        encoderState[code].push(entry); renderChips(code); markIncluded(code); encodeUGI();
      };
      addRow.appendChild(addBtn); container.appendChild(addRow);
      const chipBox = document.createElement('div'); chipBox.id = 'enc-' + code + '-chips';
      container.appendChild(chipBox);
    }
    
    function renderChips(code) {
      const box = document.getElementById('enc-' + code + '-chips');
      box.innerHTML = '';
      for (let i = 0; i < encoderState[code].length; i++) {
        const e = encoderState[code][i];
        const chip = document.createElement('span'); chip.className = 'chip';
        let txt = e.sub + e.rating;
        if (e.paid) txt += '$'; if (e.aspire) txt += '+' + e.aspire;
        chip.innerHTML = txt + ' ×';
        box.appendChild(chip);
      }
    }
    
    function removeChip(code, idx) { encoderState[code].splice(idx, 1); renderChips(code); encodeUGI(); }
    
    function buildSingleField(container, code, f) {
      const subIds = getSubIds(code); const scale = getScale(code);
      const sel = document.createElement('select');
      sel.id = 'enc-' + code + '-sub'; sel.onchange = function() { markIncluded(code); encodeUGI(); };
      const defOpt = document.createElement('option'); defOpt.value = ''; defOpt.textContent = '— select —';
      sel.appendChild(defOpt);
      for (const s of subIds) {
        const opt = document.createElement('option'); opt.value = s.code; opt.textContent = s.code + ' — ' + s.name;
        sel.appendChild(opt);
      }
      container.appendChild(sel);
      const row = document.createElement('div'); row.className = 'slider-row';
      const slider = document.createElement('input');
      slider.type = 'range'; slider.min = 0; slider.max = 7; slider.value = 4; slider.id = 'enc-' + code + '-val';
      slider.oninput = function() {
        document.getElementById('enc-' + code + '-display').textContent = this.value;
        const s = scale[this.value]; document.getElementById('enc-' + code + '-label').textContent = s ? (s.label || '') : '';
        markIncluded(code); encodeUGI();
      };
      row.appendChild(slider);
      const val = document.createElement('span'); val.className = 'slider-val'; val.id = 'enc-' + code + '-display'; val.textContent = '4';
      row.appendChild(val);
      const lab = document.createElement('span'); lab.className = 'slider-label'; lab.id = 'enc-' + code + '-label';
      lab.textContent = scale['4'] ? (scale['4'].label || '') : '';
      row.appendChild(lab); container.appendChild(row);
    }
    
    function buildGeekField(container, f) {
      const domains = getGeekDomains();
      encoderState.g = {domains: [], custom: ''};
      let lastGroup = ''; let gridDiv = null;
      for (const d of domains) {
        if (d.group && d.group !== lastGroup) {
          const h = document.createElement('h3'); h.textContent = d.group; container.appendChild(h); lastGroup = d.group;
          gridDiv = document.createElement('div');
          gridDiv.style.display = 'grid'; gridDiv.style.gridTemplateColumns = 'repeat(auto-fill, minmax(220px, 1fr))'; gridDiv.style.gap = '0.1rem 0';
          container.appendChild(gridDiv);
        }
        const lbl = document.createElement('label');
        lbl.innerHTML = ' ' + d.code + ' ' + d.name + '  $';
        (gridDiv || container).appendChild(lbl);
      }
      const customDiv = document.createElement('div');
      customDiv.style.marginTop = '0.5rem';
      customDiv.innerHTML = '';
      container.appendChild(customDiv);
    }
    
    function updateGeek() {
      const domains = [];
      document.querySelectorAll('[data-gcode]').forEach(cb => {
        if (cb.checked) {
          const code = cb.getAttribute('data-gcode');
          const paidCb = document.querySelector('[data-gpaid="' + code + '"]');
          domains.push({code, paid: paidCb && paidCb.checked});
        }
      });
      encoderState.g = {domains, custom: (document.getElementById('enc-g-custom').value || '').trim()};
      encodeUGI();
    }
    
    function buildBuildField(container, f) {
      for (const dim of ['height', 'width']) {
        const vals = f.dimensions[dim].values;
        const row = document.createElement('div'); row.className = 'slider-row';
        row.innerHTML = '' + dim + ':';
        const slider = document.createElement('input');
        slider.type = 'range'; slider.min = 0; slider.max = 7; slider.value = 4; slider.id = 'enc-b-' + dim;
        const disp = document.createElement('span'); disp.className = 'slider-val'; disp.id = 'enc-b-' + dim + '-display'; disp.textContent = '4';
        const lab = document.createElement('span'); lab.className = 'slider-label'; lab.id = 'enc-b-' + dim + '-label'; lab.textContent = vals['4'] || '';
        slider.oninput = function() { disp.textContent = this.value; lab.textContent = vals[this.value] || ''; markIncluded('b'); encodeUGI(); };
        row.appendChild(slider); row.appendChild(disp); row.appendChild(lab); container.appendChild(row);
      }
      const aDiv = document.createElement('div');
      aDiv.innerHTML = '';
      container.appendChild(aDiv);
    }
    
    function buildPoliticsField(container, f) {
      for (const dim of ['social', 'economic']) {
        const vals = f.dimensions[dim].values;
        const row = document.createElement('div'); row.className = 'slider-row';
        row.innerHTML = '' + dim + ':';
        const slider = document.createElement('input');
        slider.type = 'range'; slider.min = 0; slider.max = 7; slider.value = 4; slider.id = 'enc-v-' + dim;
        const disp = document.createElement('span'); disp.className = 'slider-val'; disp.id = 'enc-v-' + dim + '-display'; disp.textContent = '4';
        const lab = document.createElement('span'); lab.className = 'slider-label'; lab.id = 'enc-v-' + dim + '-label'; lab.textContent = vals['4'] || '';
        slider.oninput = function() { disp.textContent = this.value; lab.textContent = vals[this.value] || ''; markIncluded('v'); encodeUGI(); };
        row.appendChild(slider); row.appendChild(disp); row.appendChild(lab); container.appendChild(row);
      }
    }
    
    function buildLangField(container, f) {
      encoderState.l = [];
      const mods = f.modifiers || {}; const scale = getScale('l');
      const addRow = document.createElement('div'); addRow.className = 'multi-add';
      const codes = f.common_codes || {};
      const sel = document.createElement('select'); sel.id = 'enc-l-sel';
      const defOpt = document.createElement('option'); defOpt.value = ''; defOpt.textContent = '— select or type —';
      sel.appendChild(defOpt);
      for (const [k,v] of Object.entries(codes)) {
        const opt = document.createElement('option'); opt.value = k; opt.textContent = k + ' — ' + v; sel.appendChild(opt);
      }
      addRow.appendChild(sel);
      const customInput = document.createElement('input');
      customInput.type = 'text'; customInput.size = 4; customInput.maxLength = 2; customInput.placeholder = 'or ISO'; customInput.id = 'enc-l-custom';
      addRow.appendChild(customInput);
      const rSlider = document.createElement('input');
      rSlider.type = 'range'; rSlider.min = 0; rSlider.max = 7; rSlider.value = 5; rSlider.id = 'enc-l-rating';
      addRow.appendChild(rSlider);
      const rVal = document.createElement('span'); rVal.className = 'slider-val'; rVal.id = 'enc-l-rating-display'; rVal.textContent = '5';
      addRow.appendChild(rVal);
      const rLabel = document.createElement('span'); rLabel.className = 'slider-label'; rLabel.id = 'enc-l-rating-label';
      rLabel.textContent = scale['5'] ? (scale['5'].label || '') : '';
      rSlider.oninput = function() { rVal.textContent = this.value; const s = scale[this.value]; rLabel.textContent = s ? (s.label || '') : ''; };
      addRow.appendChild(rLabel);
      if (mods.paid) { const pCb = document.createElement('label'); pCb.innerHTML = ' $'; addRow.appendChild(pCb); }
      if (mods.aspire) {
        const aCb = document.createElement('label'); aCb.innerHTML = ' +'; addRow.appendChild(aCb);
        const aSel = document.createElement('select'); aSel.id = 'enc-l-add-aspire';
        for (let i = 5; i <= 7; i++) { const opt = document.createElement('option'); opt.value = i; opt.textContent = i + ' — ' + (scale[i] ? (scale[i].label || '') : ''); aSel.appendChild(opt); }
        addRow.appendChild(aSel);
      }
      const addBtn = document.createElement('button'); addBtn.textContent = 'add';
      addBtn.onclick = function() {
        let langCode = sel.value || customInput.value.toLowerCase().trim();
        if (!langCode || langCode.length !== 2) return;
        const entry = {sub: langCode, rating: parseInt(document.getElementById('enc-l-rating').value)};
        if (mods.paid) { const pEl = document.getElementById('enc-l-add-paid'); if (pEl && pEl.checked) entry.paid = true; }
        if (mods.aspire) { const aOn = document.getElementById('enc-l-add-aspire-on'); const aVal = document.getElementById('enc-l-add-aspire'); if (aOn && aOn.checked) entry.aspire = parseInt(aVal.value); }
        encoderState.l.push(entry); renderChips('l'); markIncluded('l'); encodeUGI();
      };
      addRow.appendChild(addBtn); container.appendChild(addRow);
      const chipBox = document.createElement('div'); chipBox.id = 'enc-l-chips'; container.appendChild(chipBox);
    }
    
    function encodeUGI() {
      const handle = document.getElementById('enc-handle') ? document.getElementById('enc-handle').value.trim() : '';
      if (!handle) {
        if (document.getElementById('enc-uri-output')) document.getElementById('enc-uri-output').textContent = '(enter a handle)';
        if (document.getElementById('enc-block-output')) document.getElementById('enc-block-output').textContent = '';
        return;
      }
      const version = (document.getElementById('enc-version') ? document.getElementById('enc-version').value.trim() : '') || '0';
      const parts = []; const blockParts = [];
      const codes = Object.keys(REG.fields).sort();
      for (const code of codes) {
        const skipEl = document.getElementById('skip-' + code);
        if (skipEl && !skipEl.checked) continue;
        const f = REG.fields[code]; const ftype = f.type;
        if (ftype === 'direct') {
          const valEl = document.getElementById('enc-' + code + '-val'); if (!valEl) continue;
          let s = code + valEl.value; let bs = code.toUpperCase() + valEl.value;
          const mods = f.modifiers || {};
          if (mods.alternative) { const altOn = document.getElementById('enc-' + code + '-alt-on'); if (altOn && altOn.checked) { const altVal = document.getElementById('enc-' + code + '-alt'); s += '/' + altVal.value; bs += '/' + altVal.value; } }
          if (mods.paid) { const pEl = document.getElementById('enc-' + code + '-paid'); if (pEl && pEl.checked) { s += '$'; bs += '$'; } }
          if (mods.aspire) { const aOn = document.getElementById('enc-' + code + '-aspire-on'); if (aOn && aOn.checked) { const aVal = document.getElementById('enc-' + code + '-aspire').value; s += '+' + aVal; bs += '+' + aVal; } }
          parts.push(s); blockParts.push(bs);
        } else if (ftype === 'multi') {
          const entries = encoderState[code] || []; if (entries.length === 0) continue;
          let uri = code; const bps = [];
          for (const e of entries) { let frag = e.sub + e.rating; if (e.paid) frag += '$'; if (e.aspire) frag += '+' + e.aspire; uri += frag; bps.push(code.toUpperCase() + frag); }
          parts.push(uri); blockParts.push(...bps);
        } else if (ftype === 'single') {
          const subEl = document.getElementById('enc-' + code + '-sub'); const valEl = document.getElementById('enc-' + code + '-val');
          if (!subEl || !subEl.value) continue;
          parts.push(code + subEl.value + valEl.value); blockParts.push(code.toUpperCase() + subEl.value + valEl.value);
        } else if (ftype === 'special') {
          if (code === 'g') {
            const gState = encoderState.g || {domains: [], custom: ''};
            if (gState.domains.length === 0 && !gState.custom) continue;
            const dParts = gState.domains.map(d => d.code + (d.paid ? '$' : ''));
            if (gState.custom) dParts.push('~' + gState.custom);
            parts.push('g' + dParts.join('/')); blockParts.push('G' + dParts.join('/'));
          } else if (code === 'b') {
            const h = document.getElementById('enc-b-height'); const w = document.getElementById('enc-b-width');
            if (!h || !w) continue;
            parts.push('b' + h.value + '/' + w.value); blockParts.push('B' + h.value + '/' + w.value);
          } else if (code === 'v') {
            const s = document.getElementById('enc-v-social'); const e = document.getElementById('enc-v-economic');
            if (!s || !e) continue;
            parts.push('v' + s.value + '/' + e.value); blockParts.push('V' + s.value + '/' + e.value);
          } else if (code === 'l') {
            const entries = encoderState.l || []; if (entries.length === 0) continue;
            let uri = 'l'; const bps = [];
            for (const e of entries) { let frag = e.sub + e.rating; if (e.paid) frag += '$'; if (e.aspire) frag += '+' + e.aspire; uri += frag; bps.push('L' + frag); }
            parts.push(uri); blockParts.push(...bps);
          }
        }
      }
      const uri = 'ugi:' + version + '@' + handle + ':' + parts.join(',');
      if (document.getElementById('enc-uri-output')) document.getElementById('enc-uri-output').textContent = uri;
      const gPart = blockParts.find(p => p.startsWith('G'));
      const otherParts = blockParts.filter(p => !p.startsWith('G'));
      let block = '------- BEGIN UGI BLOCK -------\n';
      block += 'v:' + version + ' @' + handle;
      if (gPart) block += ' ' + gPart;
      block += '\n';
      const cats = {};
      for (const bp of otherParts) { const c = bp[0].toLowerCase(); const fld = REG.fields[c]; const cat = fld ? fld.category : 'other'; if (!cats[cat]) cats[cat] = []; cats[cat].push(bp); }
      const catOrder = ['identity', 'appearance', 'tech', 'stance', 'entertainment', 'lifestyle'];
      for (const cat of catOrder) { if (cats[cat] && cats[cat].length > 0) block += cats[cat].join(' ') + '\n'; }
      block += '-------- END UGI BLOCK --------';
      if (document.getElementById('enc-block-output')) document.getElementById('enc-block-output').textContent = block;
    }
    """
    
    
    def _decoder_js():
        return _shared_js() + r"""
    function decodeUGI() {
      const input = document.getElementById('dec-input').value.trim();
      const output = document.getElementById('dec-output');
      if (!input) { output.innerHTML = ''; return; }
      try {
        let uri = input;
        if (input.includes('BEGIN UGI BLOCK')) uri = blockToUri(input);
        const parsed = parseUri(uri);
        renderDecoded(parsed, output);
      } catch(e) {
        output.innerHTML = '

    parse error: ' + e.message + '

    '; } } function parseUri(uri) { let rest = uri; if (rest.toLowerCase().startsWith('ugi:')) rest = rest.substring(4); const atIdx = rest.indexOf('@'); if (atIdx < 0) throw new Error('missing @handle'); const version = rest.substring(0, atIdx); rest = rest.substring(atIdx + 1); const colonIdx = rest.indexOf(':'); if (colonIdx < 0) throw new Error('missing : after handle'); const handle = rest.substring(0, colonIdx); rest = rest.substring(colonIdx + 1); const fieldStrs = rest.split(','); const result = {version, handle, fields: []}; for (const fs of fieldStrs) { if (!fs) continue; const code = fs[0].toLowerCase(); const raw = fs.substring(1); const f = REG.fields[code]; if (!f) { result.fields.push({code, raw, name: '(unknown)', decoded: raw}); continue; } result.fields.push({code, raw, name: f.name, decoded: decodeField(code, raw, f)}); } return result; } function decodeField(code, raw, f) { const ftype = f.type; const scale = getScale(code); if (ftype === 'direct') { if (!raw) return ''; const digit = raw[0]; const label = scale[digit] ? (scale[digit].label || digit) : digit; let result = label; let rest = raw.substring(1); if (rest.includes('/')) { const parts = rest.split('/'); if (parts[1]) { const altLabel = scale[parts[1][0]] ? (scale[parts[1][0]].label || parts[1][0]) : parts[1][0]; result += ' / ' + altLabel; } } if (rest.includes('$')) result += ' [paid]'; const aspMatch = rest.match(/\+(\d)/); if (aspMatch) result += ' [aspiring to ' + aspMatch[1] + ']'; return result; } else if (ftype === 'special') { if (code === 'g') { return raw.split('/').map(d => { if (d.startsWith('~')) return 'custom:' + d.substring(1); const paid = d.endsWith('$'); const dcode = paid ? d.slice(0,-1) : d; return resolveGeekDomain(dcode) + (paid ? ' [paid]' : ''); }).join(', '); } else if (code === 'b' || code === 'v') { const parts = raw.split('/'); const dims = Object.keys(f.dimensions); return dims.map((d, i) => { const v = parts[i] ? parts[i][0] : '?'; const vals = f.dimensions[d].values; return d + ': ' + (vals[v] || v); }).join(', '); } return raw; } else if (ftype === 'multi' || ftype === 'single') { if (code === 'l') return decodeLangEntries(raw, f); return decodeMultiEntries(code, raw, f, scale); } return raw; } function decodeMultiEntries(code, raw, f, scale) { const entries = []; let i = 0; while (i < raw.length) { if (raw[i] === '~') { let j = i + 1; while (j < raw.length && /[a-zA-Z]/.test(raw[j])) j++; const sub = raw.substring(i, j); const digit = j < raw.length && /[0-7]/.test(raw[j]) ? raw[j] : '?'; let mod = ''; let k = j + 1; while (k < raw.length && /[\$\+0-9]/.test(raw[k])) { mod += raw[k]; k++; } entries.push('custom:' + sub.substring(1) + '=' + (scale[digit] ? (scale[digit].label || digit) : digit) + modStr(mod)); i = k; } else if (/[a-zA-Z]/.test(raw[i])) { let j = i; while (j < raw.length && /[a-zA-Z]/.test(raw[j])) j++; const sub = raw.substring(i, j); const digit = j < raw.length && /[0-7]/.test(raw[j]) ? raw[j] : '?'; let mod = ''; let k = j + 1; while (k < raw.length && /[\$\+0-9]/.test(raw[k])) { mod += raw[k]; k++; } entries.push(resolveSubId(code, sub, f) + '=' + (scale[digit] ? (scale[digit].label || digit) : digit) + modStr(mod)); i = k; } else { i++; } } return entries.join(', '); } function decodeLangEntries(raw, f) { const entries = []; const scale = getScale('l'); let i = 0; while (i < raw.length) { if (/[a-zA-Z]/.test(raw[i]) && i + 2 < raw.length) { const lang = raw.substring(i, i+2); const digit = raw[i+2]; let mod = ''; let k = i + 3; while (k < raw.length && /[\$\+0-9]/.test(raw[k])) { mod += raw[k]; k++; } const langName = (REG.fields.l.common_codes || {})[lang] || lang; entries.push(langName + '=' + (scale[digit] ? (scale[digit].label || digit) : digit) + modStr(mod)); i = k; } else { i++; } } return entries.join(', '); } function renderDecoded(parsed, container) { let html = '

    @' + parsed.handle + ' (v' + parsed.version + ')

    '; html += ''; for (const f of parsed.fields) { html += ''; } html += '
    codefieldrawdecoded
    ' + f.code + '' + f.name + '' + f.raw + '' + f.decoded + '
    '; container.innerHTML = html; } function blockToUri(block) { const lines = block.split(/\r?\n/).map(l => l.trim()).filter(l => l && !l.startsWith('---')); if (lines.length === 0) throw new Error('empty block'); const firstLine = lines[0]; const vMatch = firstLine.match(/^v:(\S+)\s+@(\S+)(?:\s+(.*))?$/i); if (!vMatch) throw new Error('invalid first line'); const version = vMatch[1]; const handle = vMatch[2]; const gPart = vMatch[3] || ''; const blockFields = []; if (gPart) blockFields.push(gPart); for (let i = 1; i < lines.length; i++) blockFields.push(...lines[i].split(/\s+/)); const fieldMap = {}; for (const bf of blockFields) { if (!bf) continue; const code = bf[0].toLowerCase(); const rest = bf.substring(1); if (!fieldMap[code]) fieldMap[code] = ''; fieldMap[code] += rest; } const parts = []; for (const code of Object.keys(fieldMap).sort()) parts.push(code + fieldMap[code].toLowerCase()); return 'ugi:' + version + '@' + handle + ':' + parts.join(','); } """ def _converter_js(): return _shared_js() + r""" function convertUGI() { const input = document.getElementById('conv-input').value.trim(); const output = document.getElementById('conv-output'); if (!input) { output.textContent = ''; return; } try { if (input.includes('BEGIN UGI BLOCK')) output.textContent = blockToUri(input); else if (input.toLowerCase().startsWith('ugi:')) output.textContent = uriToBlock(input); else output.textContent = '(could not detect format — paste a URI starting with "ugi:" or a block)'; } catch(e) { output.textContent = 'error: ' + e.message; } } function blockToUri(block) { const lines = block.split(/\r?\n/).map(l => l.trim()).filter(l => l && !l.startsWith('---')); if (lines.length === 0) throw new Error('empty block'); const vMatch = lines[0].match(/^v:(\S+)\s+@(\S+)(?:\s+(.*))?$/i); if (!vMatch) throw new Error('invalid first line'); const version = vMatch[1]; const handle = vMatch[2]; const gPart = vMatch[3] || ''; const blockFields = []; if (gPart) blockFields.push(gPart); for (let i = 1; i < lines.length; i++) blockFields.push(...lines[i].split(/\s+/)); const fieldMap = {}; for (const bf of blockFields) { if (!bf) continue; const code = bf[0].toLowerCase(); if (!fieldMap[code]) fieldMap[code] = ''; fieldMap[code] += bf.substring(1); } const parts = []; for (const code of Object.keys(fieldMap).sort()) parts.push(code + fieldMap[code].toLowerCase()); return 'ugi:' + version + '@' + handle + ':' + parts.join(','); } function uriToBlock(uri) { let rest = uri; if (rest.toLowerCase().startsWith('ugi:')) rest = rest.substring(4); const atIdx = rest.indexOf('@'); const version = rest.substring(0, atIdx); rest = rest.substring(atIdx + 1); const colonIdx = rest.indexOf(':'); const handle = rest.substring(0, colonIdx); rest = rest.substring(colonIdx + 1); const fieldStrs = rest.split(','); let block = '------- BEGIN UGI BLOCK -------\n'; let firstLine = 'v:' + version + ' @' + handle; const blockParts = []; for (const fs of fieldStrs) { if (!fs) continue; const code = fs[0].toLowerCase(); const raw = fs.substring(1); const f = REG.fields[code]; if (!f) { blockParts.push({code, parts: [code.toUpperCase() + raw]}); continue; } if (code === 'g') { firstLine += ' G' + raw; continue; } const ftype = f.type; if (ftype === 'multi' || (ftype === 'special' && code === 'l')) { blockParts.push({code, parts: expandMultiToBlock(code, raw), cat: f.category}); } else { blockParts.push({code, parts: [code.toUpperCase() + raw], cat: f.category}); } } block += firstLine + '\n'; const cats = {}; for (const bp of blockParts) { const cat = bp.cat || 'other'; if (!cats[cat]) cats[cat] = []; cats[cat].push(...bp.parts); } const catOrder = ['identity', 'appearance', 'tech', 'stance', 'entertainment', 'lifestyle']; for (const cat of catOrder) { if (cats[cat] && cats[cat].length > 0) block += cats[cat].join(' ') + '\n'; } block += '-------- END UGI BLOCK --------'; return block; } function expandMultiToBlock(code, raw) { const parts = []; const uc = code.toUpperCase(); let i = 0; while (i < raw.length) { if (raw[i] === '~') { let j = i + 1; while (j < raw.length && /[a-zA-Z]/.test(raw[j])) j++; let sub = raw.substring(i, j); let digit = ''; if (j < raw.length && /[0-7]/.test(raw[j])) { digit = raw[j]; j++; } let mod = ''; while (j < raw.length && /[\$\+0-9]/.test(raw[j])) { mod += raw[j]; j++; } parts.push(uc + sub + digit + mod); i = j; } else if (/[a-zA-Z]/.test(raw[i])) { let j = i; while (j < raw.length && /[a-zA-Z]/.test(raw[j])) j++; let sub = raw.substring(i, j); let digit = ''; if (j < raw.length && /[0-7]/.test(raw[j])) { digit = raw[j]; j++; } let mod = ''; while (j < raw.length && /[\$\+0-9]/.test(raw[j])) { mod += raw[j]; j++; } parts.push(uc + sub + digit + mod); i = j; } else { i++; } } return parts; } """ def write(path, content): os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) with open(path, "w") as f: f.write(content) def main(): if len(sys.argv) != 3: print(f"Usage: {sys.argv[0]} ", file=sys.stderr) sys.exit(1) registry_path, output_dir = sys.argv[1], sys.argv[2] reg = json.load(open(registry_path)) write(os.path.join(output_dir, "index.html"), build_home(reg)) write(os.path.join(output_dir, "specs", "index.html"), build_specs(reg)) write(os.path.join(output_dir, "encoder", "index.html"), build_encoder(reg)) write(os.path.join(output_dir, "decoder", "index.html"), build_decoder(reg)) write(os.path.join(output_dir, "converter", "index.html"), build_converter(reg)) print(f"Generated 5 pages in {output_dir}") if __name__ == "__main__": main()