diff options
Diffstat (limited to 'web/scripts/gen_tool.py')
| -rw-r--r-- | web/scripts/gen_tool.py | 1853 |
1 files changed, 754 insertions, 1099 deletions
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 <registry.json> <output_dir> +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 = """\ +<footer> +<hr> +<a href="https://twt.gumx.cc">twt</a> / +<a href="https://git.gumx.cc">git</a> / +<a href="https://mail.gumx.cc">mail</a> / +<a href="https://list.gumx.cc">list</a> / +<a href="https://irc.gumx.cc">irc</a> / +<a href="https://files.gumx.cc">files</a> / +<a href="https://vpn.gumx.cc">vpn</a> / +<a href="https://pgp.gumx.cc">pgp</a> / +<a href="https://demo.gumx.cc">demo</a> / +<a href="https://wk.fo">wk.fo</a> +<br> +<a href="https://git.gumx.cc/ugi">source</a> / +<a href="https://gumx.cc/license">license</a> +</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"<strong>{label}</strong>") + else: + parts.append(f'<a href="{href}">{label}</a>') + return '<nav class="subnav">' + " / ".join(parts) + "</nav>" -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"""<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> -<meta name="viewport" content="width=device-width, initial-scale=1"> +<meta name="viewport" content="width=device-width,initial-scale=1"> <link rel="icon" type="image/svg+xml" href="/favicon.svg"> -<title>UGI Tool v{reg['spec']['version']}</title> +<title>{title}</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: 16px; line-height: 1.4; - max-width: 800px; margin: 0 auto; padding: 4rem 2rem; -}} -header {{ margin-bottom: 1em; }} -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: thin solid; margin: 1.25rem 0; }} -@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; color: inherit; -}} -.tab.active {{ background: white; color: inherit; 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; -}} -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; }} -footer {{ text-align: center; margin-top: 3rem; }} +{style} </style> </head> <body> - <header> <nav><strong><a href="https://gumx.cc">gumx</a></strong> / <a href="https://ugi.gumx.cc">ugi</a></nav> </header> +<main> +<h1>{h1}</h1> +{subnav(active)} +{content} +</main> +{FOOTER} +</body> +</html> +""" + + +def build_home(reg): + ver = reg["spec"]["version"] + desc = htmlmod.escape(reg["spec"]["description"]) + + examples = """<pre><code>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</code></pre> +<pre><code>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</code></pre>""" + + fields_summary = "\n".join( + f"<li><code>{code}</code> — {f['name']}</li>" + for code, f in sorted(reg["fields"].items()) + ) + + content = f"""<p>{desc}</p> +<h2>design goals</h2> +<ul> +<li>Single-letter field codes — 24 active, 2 reserved</li> +<li>Octal (0–7) rating scale</li> +<li>URI-safe characters only — no percent-encoding needed</li> +<li>Dual format — URI for machines, block for humans</li> +<li>Case-insensitive</li> +<li>Extensible via <code>~</code> custom sub-IDs</li> +</ul> +<h2>formats</h2> +<p>URI format:</p> +<pre><code>ugi:<version>[/<revision>]@<handle>:<field>,<field>,...</code></pre> +<p>Block format:</p> +<pre><code>------- BEGIN UGI BLOCK ------- +v:<version> @<handle> G<geek_types> +<field> <field> ... +-------- END UGI BLOCK --------</code></pre> +<h2>examples</h2> +{examples} +<h2>fields</h2> +<ul> +{fields_summary} +</ul> +<p>See the <a href="/specs/">specs page</a> for full field definitions.</p>""" + + return page(f"ugi v{ver}", "ugi", "about", content) + + +def build_specs(reg): + ver = reg["spec"]["version"] + out = "" + + 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 sv in reg["scale"]["values"]: + out += f'<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>" + + 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 = " <em>(required)</em>" if code == "g" else "" + out += f"<hr><h2>{htmlmod.escape(code)} — {htmlmod.escape(f['name'])}{req}</h2>" + if f.get("description"): + out += f'<p>{htmlmod.escape(f["description"])}</p>' + + meta = f'<strong>type:</strong> {f["type"]}' + if f.get("category"): + meta += f' | <strong>category:</strong> {f["category"]}' + if f.get("scale_type"): + meta += f' | <strong>scale:</strong> {f["scale_type"]}' + mods = list(f.get("modifiers", {}).keys()) + if mods: + meta += " | <strong>modifiers:</strong> " + ", ".join(mods) + out += f"<p>{meta}</p>" + + if f.get("values"): + out += '<ol start="0">' + for i in range(8): + v = f["values"].get(str(i), {}) + label = htmlmod.escape(v.get("label", "—")) + humor = v.get("humor", "") + out += f"<li><strong>{label}</strong>" + if humor: + out += f" — <em>{htmlmod.escape(humor)}</em>" + out += "</li>" + out += "</ol>" + + if f.get("scale_labels"): + out += '<ol start="0">' + for i in range(8): + label = htmlmod.escape(f["scale_labels"].get(str(i), "—")) + humor = f.get("humor_scale", {}).get(str(i), "") + out += f"<li><strong>{label}</strong>" + if humor: + out += f" — <em>{htmlmod.escape(humor)}</em>" + out += "</li>" + out += "</ol>" + elif f.get("humor_scale"): + out += "<p><em>notable:</em></p><ul>" + for k, h in f["humor_scale"].items(): + out += f"<li>{k}: {htmlmod.escape(h)}</li>" + out += "</ul>" + + if f.get("sub_ids"): + out += "<ul>" + 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"<li><code>{sid}</code> — {htmlmod.escape(str(name))}" + if long: + out += f" (block alias: <code>{long}</code>)" + out += "</li>" + out += "</ul>" + + if f.get("sub_id_groups"): + for gkey, group in f["sub_id_groups"].items(): + label = group.get("label", gkey) + out += f"<h3>{htmlmod.escape(label)}</h3><ul>" + for sid, sv in (group.get("sub_ids") or {}).items(): + name = sv.get("name", sid) if isinstance(sv, dict) else sv + out += f"<li><code>{sid}</code> — {htmlmod.escape(str(name))}</li>" + out += "</ul>" + + if f.get("dimensions"): + for dimname, dim in f["dimensions"].items(): + out += f"<h3>{htmlmod.escape(dimname)}</h3><ul>" + for k, v in (dim.get("values") or {}).items(): + out += f"<li>{k}: {htmlmod.escape(v)}</li>" + out += "</ul>" + + 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) -<h1>UGI Tool v{reg['spec']['version']}</h1> -<p>{html.escape(reg['spec']['description'])}</p> + content = f"""<p>Build a UGI string field by field. The <code>g</code> (geek specialization) field is required.</p> +<label>handle: <input type="text" id="enc-handle" placeholder="jdoe" oninput="encodeUGI()"></label> +<label>version: <input type="text" id="enc-version" value="0" size="3" oninput="encodeUGI()"></label> +<div id="enc-fields"></div> <hr> +<h2>URI output</h2> +<div class="output-box" id="enc-uri-output"></div> +<button onclick="copyOutput('enc-uri-output')">copy URI</button> +<h2>block output</h2> +<pre class="output-box" id="enc-block-output"></pre> +<button onclick="copyOutput('enc-block-output')">copy block</button> +<script> +const REG = {reg_json}; +{_encoder_js()} +buildEncoder(); +encodeUGI(); +</script>""" -<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> + return page(f"ugi v{ver} / encoder", "encoder", "encoder", content) -<hr> -<details> - <summary><strong>Quick Reference</strong></summary> - <pre id="quick-ref"></pre> -</details> -<footer> -<hr> -<a href="https://twt.gumx.cc">twt</a> / -<a href="https://git.gumx.cc">git</a> / -<a href="https://mail.gumx.cc">mail</a> / -<a href="https://list.gumx.cc">list</a> / -<a href="https://irc.gumx.cc">irc</a> / -<a href="https://files.gumx.cc">files</a> / -<a href="https://vpn.gumx.cc">vpn</a> / -<a href="https://pgp.gumx.cc">pgp</a> / -<a href="https://demo.gumx.cc">demo</a> / -<a href="https://wk.fo">wk.fo</a> -<br> -<a href="https://git.gumx.cc/ugi">source</a> / -<a href="https://gumx.cc/license">license</a> -</footer> +def build_decoder(reg): + ver = reg["spec"]["version"] + reg_json = json.dumps(reg, ensure_ascii=False) + content = f"""<p>Paste a UGI string (URI or block format) to decode it.</p> +<label>UGI string:</label> +<textarea id="dec-input" placeholder="ugi:0@jdoe:gcs$/dev,a4,b5/4,..." oninput="decodeUGI()"></textarea> +<div id="dec-output"></div> <script> const REG = {reg_json}; +{_decoder_js()} +</script>""" -/* === 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"""<p>Convert between URI and block formats.</p> +<label>paste either format:</label> +<textarea id="conv-input" placeholder="Paste URI or block format..." oninput="convertUGI()"></textarea> +<h2>converted output</h2> +<pre class="output-box" id="conv-output"></pre> +<button onclick="copyOutput('conv-output')">copy</button> +<script> +const REG = {reg_json}; +{_converter_js()} +</script>""" + + 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 = '<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) {{ + 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) {{ + } + 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++) {{ + 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 = '<input type="checkbox" id="enc-' + code + '-add-paid"> $'; addRow.appendChild(pCb); - }} - if (mods.aspire) {{ + } + 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++) {{ + 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 + ' <span class="remove" onclick="removeChip(\\''+code+'\\','+i+')">×</span>'; + 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); +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 = '<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"> $'; + 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>'; + customDiv.innerHTML = '<label>custom (~): <input type="text" id="enc-g-custom" placeholder="fermenting" oninput="updateGeek()"></label>'; 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 = '<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); - }} - + 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>'; + 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']) {{ +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 = '<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) {{ + 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 = '<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); - }} + 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() {{ + } + 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 = '<p style="color:red">Parse error: ' + e.message + '</p>'; - }} -}} + } catch(e) { + output.innerHTML = '<p style="color:red">parse error: ' + e.message + '</p>'; + } +} -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 = '<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) {{ +function renderDecoded(parsed, container) { + let html = '<h2>@' + parsed.handle + ' (v' + parsed.version + ')</h2>'; + html += '<table class="decode-table"><thead><tr><th>code</th><th>field</th><th>raw</th><th>decoded</th></tr></thead><tbody>'; + 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>'; + } + html += '</tbody></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... +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:<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> -</body> -</html>""" + +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]} <registry.json> <output.html>", file=sys.stderr) + print(f"Usage: {sys.argv[0]} <registry.json> <output_dir>", 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__": |
