aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--hooks/post-receive9
-rw-r--r--web/README.md39
-rw-r--r--web/scripts/gen_tool.py1853
3 files changed, 778 insertions, 1123 deletions
diff --git a/hooks/post-receive b/hooks/post-receive
index 8251ac0..d56f9aa 100644
--- a/hooks/post-receive
+++ b/hooks/post-receive
@@ -1,7 +1,8 @@
#!/bin/sh
set -e
cd /home/git/build/ugi
-python3 web/scripts/gen_tool.py spec/ugi_registry_v0.json /var/www/ugi.gumx.cc/index.html
+mkdir -p /var/www/ugi.gumx.cc/specs /var/www/ugi.gumx.cc/encoder /var/www/ugi.gumx.cc/decoder /var/www/ugi.gumx.cc/converter
+python3 web/scripts/gen_tool.py spec/ugi_registry_v0.json /var/www/ugi.gumx.cc/
rsync -rlptD fonts/ /var/www/ugi.gumx.cc/fonts/
cp web/favicon.svg /var/www/ugi.gumx.cc/favicon.svg
cat > /var/www/ugi.gumx.cc/404.html <<'HTMLEOF'
@@ -34,7 +35,7 @@ footer { margin-top: 3em; }
</head>
<body>
<header>
-<nav><strong><a href="https://gumx.cc">gumx</a></strong></nav>
+<nav><strong><a href="https://gumx.cc">gumx</a></strong> / <a href="https://ugi.gumx.cc">ugi</a></nav>
</header>
<main>
<h1>404</h1>
@@ -45,12 +46,16 @@ footer { margin-top: 3em; }
<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>
</body>
</html>
diff --git a/web/README.md b/web/README.md
index 50e8cef..3acccf5 100644
--- a/web/README.md
+++ b/web/README.md
@@ -1,46 +1,41 @@
# ugi.gumx.cc
-Web-based UGI (Unified Geek Identifier) encoder, decoder, and format converter.
+Web-based UGI (Unified Geek Identifier) tool: encoder, decoder, format converter, and spec reference.
-## What is this?
+## Pages
-A self-contained HTML tool for working with UGI strings — the modernized Geek Code. It provides three functions:
+- **/** — about the project and format examples
+- **/specs/** — full field-by-field spec reference
+- **/encoder/** — form-based interface to build a UGI string
+- **/decoder/** — paste a UGI string (URI or block format) and get a human-readable breakdown
+- **/converter/** — convert between URI format and block format
-- **Encoder** — form-based interface to build a UGI string by selecting fields, sub-IDs, and ratings
-- **Decoder** — paste a UGI string (URI or block format) and see a human-readable breakdown
-- **Converter** — convert between URI format (`ugi:0@handle:...`) and block format
-
-The tool is a single HTML file with no external dependencies. The full UGI registry is embedded inline.
-
-## Usage
-
-Open `index.html` in any browser. No server required.
-
-Supports `prefers-color-scheme` for automatic dark mode.
+Each page shares the same network-standard CSS and sub-navigation.
## Regenerating
-The generator script takes the registry JSON path and output file path as arguments:
+The generator takes the registry JSON and an output directory:
```bash
-python scripts/gen_tool.py /path/to/ugi_registry_v0.json index.html
+python3 scripts/gen_tool.py /path/to/ugi_registry_v0.json /output/dir/
```
-### Requirements
+This produces `index.html`, `specs/index.html`, `encoder/index.html`, `decoder/index.html`, and `converter/index.html`.
-- Python 3.8+
-- No external dependencies
+No external dependencies. Python 3.8+.
## Project structure
```
-ugi.gumx.cc/
-├── index.html # generated tool (deploy this)
+web/
├── scripts/
-│ └── gen_tool.py # HTML tool generator
+│ └── gen_tool.py # multi-page generator
+├── favicon.svg
└── README.md
```
+Generated HTML files are not committed — they are produced by the post-receive hook on the server.
+
## License
MIT
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:&lt;version&gt;[/&lt;revision&gt;]@&lt;handle&gt;:&lt;field&gt;,&lt;field&gt;,...</code></pre>
+<p>Block format:</p>
+<pre><code>------- BEGIN UGI BLOCK -------
+v:&lt;version&gt; @&lt;handle&gt; G&lt;geek_types&gt;
+&lt;field&gt; &lt;field&gt; ...
+-------- 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+')">&times;</span>';
+ if (e.paid) txt += '$'; if (e.aspire) txt += '+' + e.aspire;
+ chip.innerHTML = txt + ' <span class="remove" onclick="removeChip(\'' + code + '\',' + i + ')">&times;</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__":