From ba5a9613f6c5b7d9b5f7eb7add147a9bdb8b6a44 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 19 Jun 2026 20:54:08 +0300 Subject: fix: style and site structure --- web/scripts/gen_tool.py | 1853 +++++++++++++++++++---------------------------- 1 file changed, 754 insertions(+), 1099 deletions(-) (limited to 'web/scripts') diff --git a/web/scripts/gen_tool.py b/web/scripts/gen_tool.py index 4d6336d..bbafcfb 100644 --- a/web/scripts/gen_tool.py +++ b/web/scripts/gen_tool.py @@ -1,271 +1,437 @@ #!/usr/bin/env python3 -"""Generate UGI_TOOL_v0.html from ugi_registry_v0.json.""" +"""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 +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 load_registry(path): - with open(path, "r") as f: - return json.load(f) +def subnav(active): + parts = [] + for key, href, label in PAGES: + if key == active: + parts.append(f"{label}") + else: + parts.append(f'{label}') + return '" -def generate_html(reg): - reg_json = json.dumps(reg, ensure_ascii=False) +def page(title, h1, active, content, extra_style=""): + style = STYLE + ("\n" + extra_style if extra_style else "") return f""" - + -UGI Tool v{reg['spec']['version']} +{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

    +
      +
    • Single-letter field codes — 24 active, 2 reserved
    • +
    • Octal (0–7) rating scale
    • +
    • URI-safe characters only — no percent-encoding needed
    • +
    • Dual format — URI for machines, block for humans
    • +
    • Case-insensitive
    • +
    • Extensible via ~ custom sub-IDs
    • +
    +

    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

    +
      +{fields_summary} +
    +

    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:

      " + for k, h in f["humor_scale"].items(): + out += f"
    • {k}: {htmlmod.escape(h)}
    • " + out += "
    " + + if f.get("sub_ids"): + out += "
      " + for sid, sv in f["sub_ids"].items(): + name = sv.get("name", sid) if isinstance(sv, dict) else sv + long = sv.get("long", "") if isinstance(sv, dict) else "" + out += f"
    • {sid} — {htmlmod.escape(str(name))}" + if long: + out += f" (block alias: {long})" + out += "
    • " + 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)}

      " + for sid, sv in (group.get("sub_ids") or {}).items(): + name = sv.get("name", sid) if isinstance(sv, dict) else sv + out += f"
    • {sid} — {htmlmod.escape(str(name))}
    • " + out += "
    " + + if f.get("dimensions"): + for dimname, dim in f["dimensions"].items(): + out += f"

    {htmlmod.escape(dimname)}

      " + for k, v in (dim.get("values") or {}).items(): + out += f"
    • {k}: {htmlmod.escape(v)}
    • " + out += "
    " + + 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) -

    UGI Tool v{reg['spec']['version']}

    -

    {html.escape(reg['spec']['description'])}

    + content = f"""

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

    + + +

    +

    URI output

    +
    + +

    block output

    +
    
    +
    +"""
     
    -
    - - - - -
    - -
    -

    Encode your UGI

    - - - - - -
    - -
    - -
    - -
    -

    URI output:

    -
    - - -

    Block output:

    -
    
    -  
    -
    - -
    -

    Decode a UGI string

    - - -
    -
    - -
    -

    Convert between formats

    - - -

    Converted output:

    -
    
    -  
    -
    - -
    -
    -
    + return page(f"ugi v{ver} / encoder", "encoder", "encoder", content) -
    -
    - Quick Reference -
    
    -
    - +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.

    + + +
    """ -/* === Tab switching === */ -function switchTab(name) {{ - document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); - document.querySelectorAll('.tab').forEach(el => el.classList.remove('active')); - document.getElementById('tab-' + name).classList.add('active'); - document.querySelector('[onclick="switchTab(\\'' + name + '\\')"]').classList.add('active'); -}} + return page(f"ugi v{ver} / decoder", "decoder", "decoder", content) -/* === Utility === */ -function copyOutput(id) {{ + +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) {{
    +function getScale(field) {
       const f = REG.fields[field];
    -  if (!f) return {{}};
    +  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}};
    +  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}};
    +  if (st && st !== 'custom') {
    +    const out = {};
    +    for (const v of REG.scale.values) out[v.value] = {label: v[st] || v.general};
         return out;
    -  }}
    -  return {{}};
    -}}
    +  }
    +  return {};
    +}
     
    -function getSubIds(field) {{
    +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)) {{
    +  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)) {{
    +      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)) {{
    +      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}});
    -    }}
    -  }}
    +        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() {{
    +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)) {{
    +  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)) {{
    +      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}});
    -      }}
    -    }}
    -  }}
    +        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;
    +}
    +"""
     
    -/* === Encoder state === */
    -const encoderState = {{}};
     
    -function markIncluded(code) {{
    +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() {{
    +function buildEncoder() {
       const container = document.getElementById('enc-fields');
       const allCodes = Object.keys(REG.fields).sort();
    -  // Put g first (required field)
       const codes = ['g', ...allCodes.filter(c => c !== 'g')];
     
    -  for (const code of codes) {{
    +  for (const code of codes) {
         const f = REG.fields[code];
         const isRequired = code === 'g';
         const section = document.createElement('details');
    @@ -283,1116 +449,605 @@ function buildEncoder() {{
         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 (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;
    +  }
    +}
     
    -    // Default: unchecked (not included) except g (always checked)
    -    if (code !== 'g') {{
    -      document.getElementById(skipId).checked = false;
    -    }}
    -  }}
    -}}
    -
    -function buildDirectField(container, code, f) {{
    +function buildDirectField(container, code, f) {
       const scale = getScale(code);
    -  const mods = f.modifiers || {{}};
    -
    +  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() {{
    +  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();
    -  }};
    +    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) {{
    +  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) {{
    +    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) {{
    +  }
    +  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++) {{
    +    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) {{
    +    }
    +    aSel.onchange = function() { encodeUGI(); };
    +    aDiv.appendChild(aSel); container.appendChild(aDiv);
    +  }
    +}
    +
    +function buildMultiField(container, code, f) {
       const subIds = getSubIds(code);
    -  const mods = f.modifiers || {{}};
    +  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) {{
    +  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;
    -    }}
    +      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);
    -    }}
    -  }}
    +    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';
    +  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.className = 'slider-label'; rLabel.id = 'enc-' + code + '-rating-label';
       rLabel.textContent = scale['5'] ? (scale['5'].label || '') : '';
    -  rSlider.oninput = function() {{
    +  rSlider.oninput = function() {
         rVal.textContent = this.value;
    -    const s = scale[this.value];
    -    rLabel.textContent = s ? (s.label || '') : '';
    -  }};
    +    const s = scale[this.value]; rLabel.textContent = s ? (s.label || '') : '';
    +  };
       addRow.appendChild(rLabel);
    -
    -  if (mods.paid) {{
    +  if (mods.paid) {
         const pCb = document.createElement('label');
         pCb.innerHTML = ' $';
         addRow.appendChild(pCb);
    -  }}
    -  if (mods.aspire) {{
    +  }
    +  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++) {{
    +    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 rating = rSlider.value;
    -    const entry = {{ sub: subId, rating: parseInt(rating) }};
    -    if (mods.paid) {{
    -      const pEl = document.getElementById('enc-' + code + '-add-paid');
    -      if (pEl && pEl.checked) entry.paid = true;
    -    }}
    -    if (mods.aspire) {{
    +  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';
    +    }
    +    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) {{
    +function renderChips(code) {
       const box = document.getElementById('enc-' + code + '-chips');
       box.innerHTML = '';
    -  for (let i = 0; i < encoderState[code].length; i++) {{
    +  for (let i = 0; i < encoderState[code].length; i++) {
         const e = encoderState[code][i];
    -    const chip = document.createElement('span');
    -    chip.className = 'chip';
    +    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 + ' ×';
    +    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);
    +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.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;
    +  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 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() {{
    +  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();
    -  }};
    +    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';
    +  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';
    +  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);
    -}}
    +  row.appendChild(lab); container.appendChild(row);
    +}
     
    -function buildGeekField(container, f) {{
    +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;
    +  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';
    +      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 +
    -      '  $';
    +    lbl.innerHTML = ' ' + d.code + ' ' + d.name + '  $';
         (gridDiv || container).appendChild(lbl);
    -  }}
    -
    +  }
       const customDiv = document.createElement('div');
       customDiv.style.marginTop = '0.5rem';
    -  customDiv.innerHTML = '';
    +  customDiv.innerHTML = '';
       container.appendChild(customDiv);
    -}}
    +}
     
    -function updateGeek() {{
    +function updateGeek() {
       const domains = [];
    -  document.querySelectorAll('[data-gcode]').forEach(cb => {{
    -    if (cb.checked) {{
    +  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()
    -  }};
    +      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']) {{
    +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';
    +    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);
    -  }}
    -
    +    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 = '';
    +  aDiv.innerHTML = '';
       container.appendChild(aDiv);
    -}}
    +}
     
    -function buildPoliticsField(container, f) {{
    -  for (const dim of ['social', 'economic']) {{
    +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';
    +    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) {{
    +    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 —';
    +  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);
    -  }}
    +  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';
    +  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';
    +  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';
    +  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';
    +  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 || '') : '';
    -  }};
    +  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);
    -    }}
    +  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() {{
    +  }
    +  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 rating = document.getElementById('enc-l-rating').value;
    -    const entry = {{ sub: langCode, rating: parseInt(rating) }};
    -    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);
    -}}
    -
    -/* === Encode === */
    -function encodeUGI() {{
    -  const handle = document.getElementById('enc-handle').value.trim();
    -  const version = document.getElementById('enc-version').value.trim() || '0';
    -  if (!handle) {{
    -    document.getElementById('enc-uri-output').textContent = '(enter a handle)';
    -    document.getElementById('enc-block-output').textContent = '';
    +    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 parts = [];
    -  const blockParts = [];
    -
    +  }
    +  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) {{
    +  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');
    +    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;
    -      const s = code + subEl.value + valEl.value;
    -      const bs = code.toUpperCase() + subEl.value + valEl.value;
    -      parts.push(s);
    -      blockParts.push(bs);
    -
    -    }} else if (ftype === 'special') {{
    -      if (code === 'g') {{
    -        const gState = encoderState.g || {{ domains: [], custom: '' }};
    +      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);
    -        const gStr = 'g' + dParts.join('/');
    -        parts.push(gStr);
    -        blockParts.push('G' + dParts.join('/'));
    -
    -      }} else if (code === 'b') {{
    -        const h = document.getElementById('enc-b-height');
    -        const w = document.getElementById('enc-b-width');
    +        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');
    +        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);
    -      }}
    -    }}
    -  }}
    -
    -  // Build URI
    +        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(',');
    -  document.getElementById('enc-uri-output').textContent = uri;
    -
    -  // Build block
    -  // G field goes on first line
    +  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';
    +  let block = '------- BEGIN UGI BLOCK -------\n';
       block += 'v:' + version + ' @' + handle;
       if (gPart) block += ' ' + gPart;
    -  block += '\\n';
    -
    -  // Group by category
    -  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);
    -  }}
    -
    +  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';
    -    }}
    -  }}
    -
    +  for (const cat of catOrder) { if (cats[cat] && cats[cat].length > 0) block += cats[cat].join(' ') + '\n'; }
       block += '-------- END UGI BLOCK --------';
    -  document.getElementById('enc-block-output').textContent = block;
    -}}
    +  if (document.getElementById('enc-block-output')) document.getElementById('enc-block-output').textContent = block;
    +}
    +"""
    +
     
    -/* === Decoder === */
    -function decodeUGI() {{
    +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 {{
    +  if (!input) { output.innerHTML = ''; return; }
    +  try {
         let uri = input;
    -    // Detect block format
    -    if (input.includes('BEGIN UGI BLOCK')) {{
    -      uri = blockToUri(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 + '

    '; - }} -}} + } catch(e) { + output.innerHTML = '

    parse error: ' + e.message + '

    '; + } +} -function parseUri(uri) {{ +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 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) {{ + const result = {version, handle, fields: []}; + for (const fs of fieldStrs) { if (!fs) continue; - const code = fs[0].toLowerCase(); - const raw = fs.substring(1); + 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) }}); - }} - + 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') {{ +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 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; - rest = parts[1].substring(1); - }} - }} else {{ - // modifiers - rest = raw.substring(1); - }} + 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] + ']'; + 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 => {{ + } 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; - const name = resolveGeekDomain(dcode); - return name + (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(', '); - }} + 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') {{ - // l uses ISO 639-1 codes with common_codes lookup, not sub_ids + } 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++; }} - const label = scale[digit] ? (scale[digit].label || digit) : digit; - entries.push('custom:' + sub.substring(1) + '=' + label + 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++; }} - const name = resolveSubId(code, sub, f); - const label = scale[digit] ? (scale[digit].label || digit) : digit; - entries.push(name + '=' + label + modStr(mod)); - i = k; - }} else {{ - i++; - }} - }} +} + +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; - const label = scale[digit] ? (scale[digit].label || digit) : digit; - entries.push(langName + '=' + label + modStr(mod)); - i = k; - }} else {{ - i++; - }} - }} +} + +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 modStr(mod) {{ - let s = ''; - if (mod.includes('$')) s += ' [paid]'; - const m = mod.match(/\\+(\\d)/); - if (m) s += ' [aspiring to ' + m[1] + ']'; - return s; -}} - -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 renderDecoded(parsed, container) {{ - let html = '

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

    '; - html += ''; - for (const f of parsed.fields) {{ +function renderDecoded(parsed, container) { + let html = '

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

    '; + html += '
    CodeFieldRawDecoded
    '; + for (const f of parsed.fields) { html += ''; - }} - html += '
    codefieldrawdecoded
    ' + f.code + '' + f.name + '' + f.raw + '' + f.decoded + '
    '; + } + html += ''; container.innerHTML = html; -}} +} -/* === Converter === */ -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'); - - // First line: v:0 @handle G... +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] || ''; - - // Remaining lines: space-separated block fields + 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+/)); - }} - - // Group by field code (first letter), merge multi entries - 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; - }} - + 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 = []; - // Sort by field code - for (const code of Object.keys(fieldMap).sort()) {{ - parts.push(code + fieldMap[code].toLowerCase()); - }} + 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) {{ +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 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 block = '------- BEGIN UGI BLOCK -------\n'; let firstLine = 'v:' + version + ' @' + handle; - const blockParts = []; - for (const fs of fieldStrs) {{ + for (const fs of fieldStrs) { if (!fs) continue; - const code = fs[0].toLowerCase(); - const raw = fs.substring(1); + 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; - }} - + 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')) {{ - // Expand: split sub-id entries - const expanded = expandMultiToBlock(code, raw); - blockParts.push({{ code, parts: expanded, cat: f.category }}); - }} else {{ - blockParts.push({{ code, parts: [code.toUpperCase() + raw], cat: f.category }}); - }} - }} - - block += firstLine + '\\n'; - - // Group by category - const cats = {{}}; - for (const bp of blockParts) {{ - const cat = bp.cat || 'other'; - if (!cats[cat]) cats[cat] = []; - cats[cat].push(...bp.parts); - }} - + 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'; - }} - }} - + 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++; - }} - }} +} + +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; -}} - -/* === Quick Reference === */ -function buildQuickRef() {{ - const fields = REG.fields; - const codes = Object.keys(fields).sort(); - let ref = 'FORMAT: ugi:[/]@:\\n'; - ref += 'SCALE: 0=hostile 1=dislike 2=meh 3=slight- 4=neutral 5=like 6=strong 7=obsessed\\n'; - ref += 'MODIFY: $ = paid + = aspire / = fluctuate,separate ~ = custom\\n\\n'; - - const allCodes = []; - for (const c of 'abcdefghijklmnopqrstuvwxyz') {{ - if (fields[c]) {{ - allCodes.push(c + ' ' + fields[c].name.toLowerCase()); - }} else if (c === 'n' || c === 'u') {{ - allCodes.push('[' + c + ' reserved]'); - }} - }} - - for (let i = 0; i < allCodes.length; i += 5) {{ - ref += allCodes.slice(i, i+5).map(s => s.padEnd(16)).join('') + '\\n'; - }} - - document.getElementById('quick-ref').textContent = ref; -}} - -/* === Spec tab === */ -function buildSpec() {{ - const el = document.getElementById('spec-content'); - let out = ''; - - // Global scale table - out += '

    Global Scale (0–7)

    '; - out += ''; - for (const sv of REG.scale.values) {{ - out += ''; - }} - out += '
    #GeneralProficiencyEnthusiasmStance
    ' + sv.value + '' + sv.general + '' + sv.proficiency + '' + sv.enthusiasm + '' + sv.stance + '
    '; - - // Fields: g first, then alphabetical - 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]; - out += '

    ' + code.toUpperCase() + ' — ' + f.name; - if (code === 'g') out += ' (required)'; - out += '

    '; - if (f.description) out += '

    ' + f.description + '

    '; - - let meta = 'Type: ' + f.type; - if (f.category) meta += ' | Category: ' + f.category; - if (f.scale_type) meta += ' | Scale: ' + f.scale_type; - const mods = Object.keys(f.modifiers || {{}}); - if (mods.length) meta += ' | Modifiers: ' + mods.join(', '); - out += '

    ' + meta + '

    '; - - // Custom values (direct fields) - if (f.values) {{ - out += '
      '; - for (let i = 0; i <= 7; i++) {{ - const v = f.values[String(i)] || {{}}; - out += '
    1. ' + (v.label || '—') + ''; - if (v.humor) out += ' — ' + v.humor + ''; - out += '
    2. '; - }} - out += '
    '; - }} - - // Custom scale labels (multi / lang / single fields) - if (f.scale_labels) {{ - out += '
      '; - for (let i = 0; i <= 7; i++) {{ - const label = f.scale_labels[String(i)] || '—'; - const humor = f.humor_scale ? f.humor_scale[String(i)] : null; - out += '
    1. ' + label + ''; - if (humor) out += ' — ' + humor + ''; - out += '
    2. '; - }} - out += '
    '; - }} - - // Humor scale without scale_labels (multi fields referencing global scale) - if (!f.scale_labels && f.humor_scale) {{ - out += '

    Notable:

      '; - for (const [k, h] of Object.entries(f.humor_scale)) {{ - out += '
    • ' + k + ': ' + h + '
    • '; - }} - out += '
    '; - }} - - // Sub-IDs (flat) - if (f.sub_ids) {{ - out += '
      '; - for (const [sid, sv] of Object.entries(f.sub_ids)) {{ - out += '
    • ' + sid + ' — ' + sv.name; - if (sv.long) out += ' (block alias: ' + sv.long + ')'; - out += '
    • '; - }} - out += '
    '; - }} - - // Sub-ID groups - if (f.sub_id_groups) {{ - for (const [gkey, group] of Object.entries(f.sub_id_groups)) {{ - out += '

    ' + (group.label || gkey) + '

      '; - for (const [sid, sv] of Object.entries(group.sub_ids || {{}})) {{ - out += '
    • ' + sid + ' — ' + sv.name + '
    • '; - }} - out += '
    '; - }} - }} - - // Build / Politics dimensions - if (f.dimensions) {{ - for (const [dimName, dim] of Object.entries(f.dimensions)) {{ - out += '

    ' + dimName.charAt(0).toUpperCase() + dimName.slice(1) + '

      '; - for (const [k, v] of Object.entries(dim.values || {{}})) {{ - out += '
    • ' + k + ': ' + v + '
    • '; - }} - out += '
    '; - }} - }} - }} - - el.innerHTML = out; -}} - -/* === Init === */ -buildEncoder(); -buildQuickRef(); -buildSpec(); -encodeUGI(); - - -""" + +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) + 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) - html_content = generate_html(reg) + registry_path, output_dir = sys.argv[1], sys.argv[2] + reg = json.load(open(registry_path)) - os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) - with open(output_path, "w") as f: - f.write(html_content) + 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 {output_path}") + print(f"Generated 5 pages in {output_dir}") if __name__ == "__main__": -- cgit v1.2.3