diff options
Diffstat (limited to 'web/scripts/gen_tool.py')
| -rw-r--r-- | web/scripts/gen_tool.py | 1388 |
1 files changed, 1388 insertions, 0 deletions
diff --git a/web/scripts/gen_tool.py b/web/scripts/gen_tool.py new file mode 100644 index 0000000..232a7eb --- /dev/null +++ b/web/scripts/gen_tool.py @@ -0,0 +1,1388 @@ +#!/usr/bin/env python3 +"""Generate UGI_TOOL_v0.html from ugi_registry_v0.json.""" + +import json +import os +import sys +import html + + +def load_registry(path): + with open(path, "r") as f: + return json.load(f) + + +def generate_html(reg): + reg_json = json.dumps(reg, ensure_ascii=False) + + return f"""<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>UGI Tool v{reg['spec']['version']} — Encoder / Decoder / Converter</title> +<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; }} +* {{ box-sizing: border-box; margin: 0; padding: 0; unicode-bidi: plaintext; }} +html {{ color: black; background-color: white; }} +body {{ + font-family: "Kawkab Mono", monospace; + font-size: 14px; line-height: 1.5; + max-width: 900px; margin: 0 auto; padding: 1rem; +}} +h1 {{ font-size: 1.3rem; margin-bottom: 0.5rem; }} +h2 {{ font-size: 1.1rem; margin: 1rem 0 0.5rem; }} +h3 {{ font-size: 0.95rem; margin: 0.8rem 0 0.3rem; }} +hr {{ border: none; border-top: 1px solid; margin: 1rem 0; }} +a {{ color: inherit; }} +@media (prefers-color-scheme: dark) {{ html {{ filter: invert(1); }} img {{ filter: invert(1); }} }} +.tabs {{ + display: flex; gap: 0; border-bottom: 2px solid; margin-bottom: 1rem; +}} +.tab {{ + padding: 0.4rem 1rem; cursor: pointer; border: 1px solid transparent; + border-bottom: none; background: rgba(128,128,128,0.1); + font-family: inherit; font-size: inherit; +}} +.tab.active {{ background: white; border-color: currentColor; border-bottom: 1px solid white; margin-bottom: -2px; font-weight: bold; }} +.tab-content {{ display: none; }} +.tab-content.active {{ display: block; }} +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; border-radius: 3px; +}} +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; + border-radius: 3px; +}} +button:hover {{ opacity: 0.7; }} +.field-section {{ margin: 0.5rem 0; padding: 0.5rem; border: 1px solid; border-radius: 3px; }} +.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); border-radius: 3px; font-size: 0.85rem; +}} +.chip .remove {{ cursor: pointer; margin-left: 0.3rem; }} +.output-box {{ + background: rgba(128,128,128,0.08); padding: 0.8rem; border-radius: 3px; + 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; }} +footer {{ text-align: center; margin-top: 3rem; font-size: 0.85rem; }} +</style> +</head> +<body> + +<h1>UGI Tool v{reg['spec']['version']}</h1> +<p>{html.escape(reg['spec']['description'])}</p> +<hr> + +<div class="tabs"> + <button class="tab active" onclick="switchTab('encoder')">Encoder</button> + <button class="tab" onclick="switchTab('decoder')">Decoder</button> + <button class="tab" onclick="switchTab('converter')">Converter</button> + <button class="tab" onclick="switchTab('spec')">Spec</button> +</div> + +<div id="tab-encoder" class="tab-content active"> + <h2>Encode your UGI</h2> + + <label>Handle (required):</label> + <input type="text" id="enc-handle" placeholder="yourhandle" oninput="encodeUGI()"> + + <label>Version:</label> + <div class="slider-row"> + <input type="text" id="enc-version" value="0" size="5" oninput="encodeUGI()"> + </div> + + <div id="enc-fields"></div> + + <hr> + <h3>URI output:</h3> + <div class="output-box" id="enc-uri-output"></div> + <button onclick="copyOutput('enc-uri-output')">Copy URI</button> + + <h3>Block output:</h3> + <pre class="output-box" id="enc-block-output"></pre> + <button onclick="copyOutput('enc-block-output')">Copy Block</button> +</div> + +<div id="tab-decoder" class="tab-content"> + <h2>Decode a UGI string</h2> + <label>Paste UGI (URI or block):</label> + <textarea id="dec-input" placeholder="ugi:0@jdoe:gcs$/dev,a4,b5/4,..." oninput="decodeUGI()"></textarea> + <div id="dec-output"></div> +</div> + +<div id="tab-converter" class="tab-content"> + <h2>Convert between formats</h2> + <label>Paste either format:</label> + <textarea id="conv-input" placeholder="Paste URI or block format..." oninput="convertUGI()"></textarea> + <h3>Converted output:</h3> + <pre class="output-box" id="conv-output"></pre> + <button onclick="copyOutput('conv-output')">Copy</button> +</div> + +<div id="tab-spec" class="tab-content"> + <div id="spec-content"></div> +</div> + +<hr> +<details> + <summary><strong>Quick Reference</strong></summary> + <pre id="quick-ref"></pre> +</details> + +<script> +const REG = {reg_json}; + +/* === 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'); +}} + +/* === Utility === */ +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; +}} + +/* === Encoder state === */ +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(); + // Put g first (required field) + 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 + + '<label class="skip-check"><input type="checkbox" id="' + skipId + '" checked onchange="encodeUGI()"' + + (isRequired ? ' disabled' : '') + '> include</label>'; + 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); + + // Default: unchecked (not included) except g (always checked) + 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 = '<label><input type="checkbox" id="enc-' + code + '-alt-on" onchange="encodeUGI()"> Alternative value</label>'; + 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 = '<label><input type="checkbox" id="enc-' + code + '-paid" onchange="encodeUGI()"> Paid ($)</label>'; + container.appendChild(pDiv); + }} + + if (mods.aspire) {{ + const aDiv = document.createElement('div'); + aDiv.innerHTML = '<label><input type="checkbox" id="enc-' + code + '-aspire-on" onchange="encodeUGI()"> Aspiring to:</label>'; + 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 = '<input type="checkbox" id="enc-' + code + '-add-paid"> $'; + addRow.appendChild(pCb); + }} + if (mods.aspire) {{ + const aCb = document.createElement('label'); + aCb.innerHTML = '<input type="checkbox" id="enc-' + code + '-add-aspire-on"> +'; + 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 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) {{ + 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 + ' <span class="remove" onclick="removeChip(\\''+code+'\\','+i+')">×</span>'; + 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 = '<input type="checkbox" data-gcode="' + d.code + '" onchange="updateGeek()"> ' + + d.code + ' ' + d.name + + ' <input type="checkbox" data-gpaid="' + d.code + '" onchange="updateGeek()" title="paid ($)" style="margin-left:4px"> $'; + (gridDiv || container).appendChild(lbl); + }} + + const customDiv = document.createElement('div'); + customDiv.style.marginTop = '0.5rem'; + customDiv.innerHTML = '<label>Custom (~): <input type="text" id="enc-g-custom" placeholder="fermenting" oninput="updateGeek()"></label>'; + 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 = '<strong>' + dim + ':</strong>'; + 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 = '<label><input type="checkbox" id="enc-b-aspire-on" onchange="encodeUGI()"> Aspiring (+)</label>'; + 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 = '<strong>' + dim + ':</strong>'; + 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 = '<input type="checkbox" id="enc-l-add-paid"> $'; + addRow.appendChild(pCb); + }} + if (mods.aspire) {{ + const aCb = document.createElement('label'); + aCb.innerHTML = '<input type="checkbox" id="enc-l-add-aspire-on"> +'; + 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 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 = ''; + return; + }} + + 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; + 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: '' }}; + 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'); + 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); + }} + }} + }} + + // Build URI + const uri = 'ugi:' + version + '@' + handle + ':' + parts.join(','); + document.getElementById('enc-uri-output').textContent = uri; + + // Build block + // G field goes on first line + 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'; + + // 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); + }} + + 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 --------'; + document.getElementById('enc-block-output').textContent = block; +}} + +/* === Decoder === */ +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; + // Detect block format + if (input.includes('BEGIN UGI BLOCK')) {{ + uri = blockToUri(input); + }} + const parsed = parseUri(uri); + renderDecoded(parsed, output); + }} catch(e) {{ + output.innerHTML = '<p style="color:red">Parse error: ' + e.message + '</p>'; + }} +}} + +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; + rest = parts[1].substring(1); + }} + }} else {{ + // modifiers + rest = raw.substring(1); + }} + 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; + 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(', '); + }} + return raw; + + }} else if (ftype === 'multi' || ftype === 'single') {{ + // l uses ISO 639-1 codes with common_codes lookup, not sub_ids + 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++; + }} + }} + 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++; + }} + }} + 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 = '<h3>Decoded: @' + parsed.handle + ' (v' + parsed.version + ')</h3>'; + html += '<table class="decode-table"><tr><th>Code</th><th>Field</th><th>Raw</th><th>Decoded</th></tr>'; + for (const f of parsed.fields) {{ + html += '<tr><td>' + f.code + '</td><td>' + f.name + '</td><td><code>' + f.raw + '</code></td><td>' + f.decoded + '</td></tr>'; + }} + html += '</table>'; + 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... + 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 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; + }} + + const parts = []; + // Sort by field code + 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')) {{ + // 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); + }} + + 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; +}} + +/* === Quick Reference === */ +function buildQuickRef() {{ + const fields = REG.fields; + const codes = Object.keys(fields).sort(); + let ref = 'FORMAT: ugi:<ver>[/<rev>]@<handle>:<fields>\\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 += '<h2>Global Scale (0–7)</h2>'; + out += '<table><thead><tr><th>#</th><th>General</th><th>Proficiency</th><th>Enthusiasm</th><th>Stance</th></tr></thead><tbody>'; + for (const sv of REG.scale.values) {{ + out += '<tr><td>' + sv.value + '</td><td>' + sv.general + '</td><td>' + sv.proficiency + '</td><td>' + sv.enthusiasm + '</td><td>' + sv.stance + '</td></tr>'; + }} + out += '</tbody></table>'; + + // 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 += '<hr><h2>' + code.toUpperCase() + ' — ' + f.name; + if (code === 'g') out += ' <em>(required)</em>'; + out += '</h2>'; + if (f.description) out += '<p>' + f.description + '</p>'; + + let meta = '<strong>Type:</strong> ' + f.type; + if (f.category) meta += ' | <strong>Category:</strong> ' + f.category; + if (f.scale_type) meta += ' | <strong>Scale:</strong> ' + f.scale_type; + const mods = Object.keys(f.modifiers || {{}}); + if (mods.length) meta += ' | <strong>Modifiers:</strong> ' + mods.join(', '); + out += '<p>' + meta + '</p>'; + + // Custom values (direct fields) + if (f.values) {{ + out += '<ol start="0">'; + for (let i = 0; i <= 7; i++) {{ + const v = f.values[String(i)] || {{}}; + out += '<li><strong>' + (v.label || '—') + '</strong>'; + if (v.humor) out += ' — <em>' + v.humor + '</em>'; + out += '</li>'; + }} + out += '</ol>'; + }} + + // Custom scale labels (multi / lang / single fields) + if (f.scale_labels) {{ + out += '<ol start="0">'; + 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 += '<li><strong>' + label + '</strong>'; + if (humor) out += ' — <em>' + humor + '</em>'; + out += '</li>'; + }} + out += '</ol>'; + }} + + // Humor scale without scale_labels (multi fields referencing global scale) + if (!f.scale_labels && f.humor_scale) {{ + out += '<p><em>Notable:</em></p><ul>'; + for (const [k, h] of Object.entries(f.humor_scale)) {{ + out += '<li>' + k + ': ' + h + '</li>'; + }} + out += '</ul>'; + }} + + // Sub-IDs (flat) + if (f.sub_ids) {{ + out += '<ul>'; + for (const [sid, sv] of Object.entries(f.sub_ids)) {{ + out += '<li><code>' + sid + '</code> — ' + sv.name; + if (sv.long) out += ' (block alias: <code>' + sv.long + '</code>)'; + out += '</li>'; + }} + out += '</ul>'; + }} + + // Sub-ID groups + if (f.sub_id_groups) {{ + for (const [gkey, group] of Object.entries(f.sub_id_groups)) {{ + out += '<h3>' + (group.label || gkey) + '</h3><ul>'; + for (const [sid, sv] of Object.entries(group.sub_ids || {{}})) {{ + out += '<li><code>' + sid + '</code> — ' + sv.name + '</li>'; + }} + out += '</ul>'; + }} + }} + + // Build / Politics dimensions + if (f.dimensions) {{ + for (const [dimName, dim] of Object.entries(f.dimensions)) {{ + out += '<h3>' + dimName.charAt(0).toUpperCase() + dimName.slice(1) + '</h3><ul>'; + for (const [k, v] of Object.entries(dim.values || {{}})) {{ + out += '<li>' + k + ': ' + v + '</li>'; + }} + out += '</ul>'; + }} + }} + }} + + el.innerHTML = out; +}} + +/* === Init === */ +buildEncoder(); +buildQuickRef(); +buildSpec(); +encodeUGI(); +</script> +<footer> +<hr> +<a href="https://gumx.cc">gumx.cc</a> / +<a href="https://git.gumx.cc">git</a> / +<a href="https://mail.gumx.cc">mail</a> / +<a href="https://irc.gumx.cc">irc</a> / +<a href="https://vpn.gumx.cc">vpn</a> / +<a href="https://pgp.gumx.cc">pgp</a> / +<a href="https://wk.fo">wk.fo</a> +</footer> +</body> +</html>""" + + +def main(): + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} <registry.json> <output.html>", 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) + + os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) + with open(output_path, "w") as f: + f.write(html_content) + + print(f"Generated {output_path}") + + +if __name__ == "__main__": + main() |
