Something went wrong. Try again.
Tools for the Atmosphere tools.slices.network
quickslice atproto html
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133<!doctype html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline';" /> <title>{ Lexicon Validator }</title> <style> /* CSS Reset */ *, *::before, *::after { box-sizing: border-box; } * { margin: 0; } body { line-height: 1.5; -webkit-font-smoothing: antialiased; } input, button, textarea { font: inherit; }
/* Light Theme */ :root { --bg-primary: #f5f5f5; --bg-card: #ffffff; --bg-input: #fafafa; --text-primary: #1a1a1a; --text-secondary: #666666; --accent: #0066cc; --accent-hover: #0052a3; --border: #e0e0e0; --border-focus: #0066cc; --error-bg: #fef2f2; --error-border: #fca5a5; --error-text: #dc2626; --success-bg: #f0fdf4; --success-border: #86efac; --success-text: #16a34a; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg-primary); color: var(--text-primary); min-height: 100vh; padding: 2rem 1rem; }
#app { max-width: 700px; margin: 0 auto; }
header { text-align: center; margin-bottom: 2rem; }
header h1 { font-size: 2rem; color: var(--text-primary); margin-bottom: 0.25rem; }
.tagline { color: var(--text-secondary); font-size: 0.875rem; }
.tagline a { color: var(--accent); text-decoration: none; }
.tagline a:hover { text-decoration: underline; }
/* Sections */ .section { background: var(--bg-card); border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); }
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; }
.section-title { font-weight: 600; font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-secondary); }
/* Buttons */ .btn { padding: 0.5rem 1rem; border: none; border-radius: 0.375rem; font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: background-color 0.15s, opacity 0.15s; }
.btn-primary { background: var(--accent); color: #ffffff; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-secondary { background: var(--bg-card); color: var(--text-primary); border: 1px solid var(--border); }
.btn-secondary:hover { background: var(--bg-primary); }
.btn-small { padding: 0.25rem 0.5rem; font-size: 0.75rem; }
.btn-danger { background: var(--error-bg); color: var(--error-text); border: 1px solid var(--error-border); }
.btn-danger:hover { background: #fee2e2; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Editors */ .editor-card { background: var(--bg-input); border: 1px solid var(--border); border-radius: 0.375rem; margin-bottom: 0.75rem; }
.editor-header { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); background: var(--bg-card); border-radius: 0.375rem 0.375rem 0 0; }
.editor-label { font-size: 0.75rem; color: var(--text-secondary); font-weight: 500; }
textarea { width: 100%; min-height: 200px; padding: 0.75rem; background: var(--bg-input); border: none; color: var(--text-primary); font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; font-size: 0.8125rem; resize: vertical; border-radius: 0 0 0.375rem 0.375rem; }
textarea:focus { outline: none; background: #ffffff; }
textarea::placeholder { color: var(--text-secondary); opacity: 0.6; }
/* NSID Input */ .nsid-row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; }
.nsid-label { font-size: 0.875rem; color: var(--text-secondary); white-space: nowrap; }
input[type="text"], select { flex: 1; padding: 0.5rem 0.75rem; background: var(--bg-input); border: 1px solid var(--border); border-radius: 0.375rem; color: var(--text-primary); font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; font-size: 0.8125rem; }
input[type="text"]:focus, select:focus { outline: none; border-color: var(--border-focus); background: #ffffff; }
input[type="text"]::placeholder { color: var(--text-secondary); opacity: 0.6; }
select:disabled { opacity: 0.6; cursor: not-allowed; }
/* Results */ .result { padding: 1rem; border-radius: 0.375rem; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; font-size: 0.8125rem; white-space: pre-wrap; word-break: break-word; }
.result-success { background: var(--success-bg); border: 1px solid var(--success-border); color: var(--success-text); }
.result-error { background: var(--error-bg); border: 1px solid var(--error-border); color: var(--error-text); }
.result:empty { display: none; }
/* Button row */ .button-row { display: flex; gap: 0.5rem; margin-top: 0.75rem; }
.hidden { display: none !important; }
/* Drop Zone */ .drop-zone { border: 2px dashed var(--border); border-radius: 0.5rem; padding: 2rem; text-align: center; margin-bottom: 1rem; transition: all 0.2s ease; cursor: pointer; }
.drop-zone:hover { border-color: var(--accent); background: var(--bg-input); }
.drop-zone.drag-over { border-color: var(--accent); background: rgba(0, 102, 204, 0.05); }
.drop-zone-icon { font-size: 2rem; margin-bottom: 0.5rem; }
.drop-zone-text { color: var(--text-secondary); font-size: 0.875rem; }
.drop-zone-text strong { color: var(--accent); }
.drop-zone-hint { color: var(--text-secondary); font-size: 0.75rem; margin-top: 0.25rem; opacity: 0.8; }
.loading-spinner { display: inline-block; width: 1rem; height: 1rem; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 0.5rem; vertical-align: middle; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Collapsible editors */ .editor-card.collapsed textarea { display: none; }
.collapse-toggle { background: none; border: none; cursor: pointer; padding: 0.25rem; color: var(--text-secondary); font-size: 0.75rem; display: flex; align-items: center; gap: 0.25rem; }
.collapse-toggle:hover { color: var(--text-primary); }
.collapse-all-row { display: flex; justify-content: flex-end; margin-bottom: 0.5rem; gap: 0.5rem; } </style> </head> <body> <div id="app"> <header> <h1> <span style="color: var(--accent)">{</span> Lexicon Validator <span style="color: var(--accent)">}</span> </h1> <p class="tagline"> Powered by <a href="https://hexdocs.pm/honk/index.html" target="_blank">honk</a> </p> </header> <main> <div id="lexicons-section" class="section"></div> <div id="record-section" class="section"></div> <div id="results-section"></div> </main> </div> <script src="https://cdn.jsdelivr.net/gh/bigmoves/honk@deaa420/dist/honk.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> <script> // ============================================================================= // STATE // =============================================================================
const state = { lexicons: [{ id: 1, value: "", collapsed: false }], nextLexiconId: 2, record: "", nsid: "", availableNsids: [], result: null, };
// ============================================================================= // PLACEHOLDERS // =============================================================================
const LEXICON_PLACEHOLDER = `{ "lexicon": 1, "id": "com.example.myRecord", "defs": { "main": { "type": "record", "record": { "type": "object", "required": ["status", "createdAt"], "properties": { "status": { "type": "string", "maxLength": 100 }, "createdAt": { "type": "string", "format": "datetime" } } } } }}`;
const RECORD_PLACEHOLDER = `{ "status": "Hello world", "createdAt": "2025-01-15T12:00:00Z"}`;
// ============================================================================= // HELPERS // =============================================================================
function esc(str) { const d = document.createElement("div"); d.textContent = str || ""; return d.innerHTML; }
function escapeAttr(str) { return (str || "") .replace(/&/g, "&") .replace(/"/g, """) .replace(/</g, "<") .replace(/>/g, ">"); }
function unwrapHonkResult(result) { if (typeof result.isOk === "function") { return { ok: result.isOk(), value: result[0] }; } return { ok: true, value: result }; }
function formatHonkError(err) { if (!err) return "Unknown error"; if (typeof err === "string") return err; if (err.message) { return err.path ? `${err.message} (at ${err.path})` : err.message; }
// Handle Gleam Dict/Map structure (HAMT) // Structure: { root: { array: [{ k: nsid, v: { head: errorMsg, tail: {} } }] }, size: n } if (err.root && err.root.array && Array.isArray(err.root.array)) { const errors = []; for (const entry of err.root.array) { if (entry.v) { // Extract errors from linked list structure (head/tail) let current = entry.v; while (current && current.head) { errors.push(current.head); current = current.tail; } } } if (errors.length > 0) { return errors.join("\n"); } }
// Handle simple linked list structure (head/tail without root) if (err.head) { const errors = []; let current = err; while (current && current.head) { errors.push(current.head); current = current.tail; } if (errors.length > 0) { return errors.join("\n"); } }
return JSON.stringify(err, null, 2); }
// ============================================================================= // RENDERING // =============================================================================
function renderLexiconsSection() { const section = document.getElementById("lexicons-section");
const getLexiconLabel = (lex, idx) => { try { const obj = JSON.parse(lex.value); if (obj.id) return obj.id; } catch (e) {} return `Lexicon ${idx + 1}`; };
const editorsHtml = state.lexicons .map( (lex, idx) => ` <div class="editor-card ${lex.collapsed ? "collapsed" : ""}" data-lexicon-id="${lex.id}"> <div class="editor-header"> <div style="display: flex; align-items: center; gap: 0.5rem;"> <button class="collapse-toggle" onclick="toggleLexicon(${lex.id})"> ${lex.collapsed ? "▶" : "▼"} </button> <span class="editor-label">${esc(getLexiconLabel(lex, idx))}</span> </div> <button class="btn btn-danger btn-small" onclick="removeLexicon(${lex.id})" ${state.lexicons.length === 1 ? "disabled" : ""} >Remove</button> </div> <textarea placeholder="${escapeAttr(LEXICON_PLACEHOLDER)}" onchange="updateLexicon(${lex.id}, this.value)" oninput="updateLexicon(${lex.id}, this.value)" >${esc(lex.value)}</textarea> </div> `, ) .join("");
const hasMultiple = state.lexicons.length > 1; const allCollapsed = state.lexicons.every((l) => l.collapsed); const collapseAllRow = hasMultiple ? `<div class="collapse-all-row"> <button class="btn btn-secondary btn-small" onclick="toggleAllLexicons(${allCollapsed ? "false" : "true"})"> ${allCollapsed ? "Expand All" : "Collapse All"} </button> </div>` : "";
section.innerHTML = ` <div class="section-header"> <span class="section-title">Lexicons</span> </div> <div class="drop-zone" id="drop-zone" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" ondrop="handleDrop(event)" onclick="triggerFileInput()" > <div class="drop-zone-icon">📦</div> <div class="drop-zone-text"> <strong>Drop a .zip file</strong> or click to browse </div> <div class="drop-zone-hint">Supports nested folders with .json lexicon files</div> </div> <input type="file" id="file-input" accept=".zip" onchange="handleFileSelect(event)" style="display: none;" /> ${collapseAllRow} ${editorsHtml} <div class="button-row"> <button class="btn btn-secondary" onclick="addLexicon()">+ Add Lexicon</button> <button class="btn btn-primary" onclick="validateLexicons()">Validate Lexicons</button> </div> `; }
function renderRecordSection() { const section = document.getElementById("record-section");
const optionsHtml = state.availableNsids.length === 0 ? '<option value="" disabled selected>Enter a lexicon with a record type</option>' : [ `<option value="" disabled ${!state.nsid ? "selected" : ""}>Select a record type</option>`, ] .concat( state.availableNsids.map( (nsid) => `<option value="${escapeAttr(nsid)}" ${state.nsid === nsid ? "selected" : ""}>${esc(nsid)}</option>`, ), ) .join("");
section.innerHTML = ` <div class="section-header"> <span class="section-title">Record</span> </div> <div class="nsid-row"> <span class="nsid-label">NSID:</span> <select id="nsid-select" onchange="updateNsid(this.value)" ${state.availableNsids.length === 0 ? "disabled" : ""} >${optionsHtml}</select> </div> <div class="editor-card"> <div class="editor-header"> <span class="editor-label">Record Data</span> <button class="btn btn-secondary btn-small" onclick="resetRecordTemplate()" ${!state.nsid ? "disabled" : ""} >Reset</button> </div> <textarea id="record-input" placeholder="${escapeAttr(RECORD_PLACEHOLDER)}" onchange="updateRecord(this.value)" oninput="updateRecord(this.value)" >${esc(state.record)}</textarea> </div> <div class="button-row"> <button class="btn btn-primary" onclick="validateRecord()">Validate Record</button> </div> `; }
function renderResult() { const section = document.getElementById("results-section");
if (!state.result) { section.innerHTML = ""; return; }
const cls = state.result.success ? "result-success" : "result-error"; const icon = state.result.success ? "✓" : "✗";
section.innerHTML = ` <div class="result ${cls}">${icon} ${esc(state.result.message)}</div> `; }
// ============================================================================= // STATE UPDATES // =============================================================================
function addLexicon() { state.lexicons.push({ id: state.nextLexiconId++, value: "", collapsed: false }); renderLexiconsSection(); }
function toggleLexicon(id) { const lex = state.lexicons.find((l) => l.id === id); if (lex) { lex.collapsed = !lex.collapsed; renderLexiconsSection(); } }
function toggleAllLexicons(collapse) { state.lexicons.forEach((lex) => { lex.collapsed = collapse; }); renderLexiconsSection(); }
function removeLexicon(id) { if (state.lexicons.length <= 1) return; state.lexicons = state.lexicons.filter((l) => l.id !== id); renderLexiconsSection(); updateAvailableNsids(); }
function updateLexicon(id, value) { const lex = state.lexicons.find((l) => l.id === id); if (lex) lex.value = value; updateAvailableNsids(); }
function updateAvailableNsids() { const nsids = []; for (const lex of state.lexicons) { const trimmed = lex.value.trim(); if (!trimmed) continue; try { const obj = JSON.parse(trimmed); if (obj.id && obj.defs?.main?.type === "record") { nsids.push(obj.id); } } catch (e) { // Invalid JSON, skip } } state.availableNsids = nsids; // Clear selected NSID if it's no longer available if (state.nsid && !nsids.includes(state.nsid)) { state.nsid = ""; state.record = ""; } renderRecordSection(); }
function updateRecord(value) { state.record = value; }
function resetRecordTemplate() { if (state.nsid) { const template = generateRecordTemplate(state.nsid); if (template) { state.record = template; renderRecordSection(); } } }
function updateNsid(value) { state.nsid = value; if (value) { const template = generateRecordTemplate(value); if (template) { state.record = template; } } renderRecordSection(); }
function generateRecordTemplate(nsid) { for (const lex of state.lexicons) { const trimmed = lex.value.trim(); if (!trimmed) continue; try { const obj = JSON.parse(trimmed); if (obj.id === nsid && obj.defs?.main?.type === "record") { const record = obj.defs.main.record; if (record?.type === "object" && record.properties) { const template = {}; const required = record.required || []; for (const field of required) { const prop = record.properties[field]; if (prop) { template[field] = getDefaultValue(prop); } } return JSON.stringify(template, null, 2); } } } catch (e) { // Invalid JSON, skip } } return null; }
function getDefaultValue(prop) { switch (prop.type) { case "string": if (prop.format === "datetime") return new Date().toISOString(); if (prop.format === "uri") return "https://example.com"; if (prop.format === "at-uri") return "at://did:plc:example/app.bsky.feed.post/abc123"; if (prop.format === "did") return "did:plc:example"; if (prop.format === "handle") return "user.example.com"; if (prop.format === "cid") return "bafyreib..."; if (prop.const) return prop.const; if (prop.enum) return prop.enum[0]; return ""; case "integer": return prop.minimum ?? prop.default ?? 0; case "boolean": return prop.default ?? false; case "array": return []; case "object": return {}; case "blob": return { $type: "blob", ref: { $link: "" }, mimeType: "", size: 0 }; default: return null; } }
// ============================================================================= // VALIDATION // =============================================================================
function parseLexicons() { const parsed = []; for (let i = 0; i < state.lexicons.length; i++) { const lex = state.lexicons[i]; const trimmed = lex.value.trim();
if (!trimmed) { return { error: `Lexicon ${i + 1}: Empty - please enter a lexicon schema`, }; }
// Validate JSON syntax first try { const obj = JSON.parse(trimmed); if (Array.isArray(obj)) { return { error: `Lexicon ${i + 1}: Expected a single lexicon object, not an array. Use "+ Add Lexicon" for multiple.`, }; } } catch (e) { return { error: `Lexicon ${i + 1}: Invalid JSON - ${e.message}` }; }
// Parse to Gleam Json type const parseResult = honk.parse_json_string(trimmed); const unwrapped = unwrapHonkResult(parseResult); if (!unwrapped.ok) { return { error: `Lexicon ${i + 1}: ${formatHonkError(unwrapped.value)}` }; } parsed.push(unwrapped.value); } return { lexicons: parsed }; }
function validateLexicons() { state.result = null;
const parseResult = parseLexicons(); if (parseResult.error) { state.result = { success: false, message: parseResult.error }; renderResult(); return; }
try { // Convert JS array to Gleam List const lexiconList = honk.toList(parseResult.lexicons); const result = honk.validate(lexiconList); const unwrapped = unwrapHonkResult(result);
if (unwrapped.ok) { const count = parseResult.lexicons.length; const noun = count === 1 ? "lexicon" : "lexicons"; state.result = { success: true, message: `${count} ${noun} valid` }; } else { state.result = { success: false, message: `Validation failed: ${formatHonkError(unwrapped.value)}`, }; } } catch (e) { state.result = { success: false, message: `Validation error: ${e.message}`, }; }
renderResult(); }
function validateRecord() { state.result = null;
const nsid = state.nsid.trim(); if (!nsid) { state.result = { success: false, message: "NSID is required for record validation", }; renderResult(); return; }
if (!honk.is_valid_nsid(nsid)) { state.result = { success: false, message: `Invalid NSID format: "${nsid}"`, }; renderResult(); return; }
const parseResult = parseLexicons(); if (parseResult.error) { state.result = { success: false, message: parseResult.error }; renderResult(); return; }
const recordTrimmed = state.record.trim(); if (!recordTrimmed) { state.result = { success: false, message: "Record data is required" }; renderResult(); return; }
// Validate JSON syntax first try { JSON.parse(recordTrimmed); } catch (e) { state.result = { success: false, message: `Record: Invalid JSON - ${e.message}`, }; renderResult(); return; }
// Parse record to Gleam Json type const recordParseResult = honk.parse_json_string(recordTrimmed); const recordUnwrapped = unwrapHonkResult(recordParseResult); if (!recordUnwrapped.ok) { state.result = { success: false, message: `Record: ${formatHonkError(recordUnwrapped.value)}`, }; renderResult(); return; }
try { // Convert JS array to Gleam List const lexiconList = honk.toList(parseResult.lexicons); const result = honk.validate_record(lexiconList, nsid, recordUnwrapped.value); const unwrapped = unwrapHonkResult(result);
if (unwrapped.ok) { state.result = { success: true, message: `Record valid against ${nsid}`, }; } else { state.result = { success: false, message: `Record invalid: ${formatHonkError(unwrapped.value)}`, }; } } catch (e) { state.result = { success: false, message: `Validation error: ${e.message}`, }; }
renderResult(); }
// ============================================================================= // ZIP FILE HANDLING // =============================================================================
function handleDragOver(event) { event.preventDefault(); event.stopPropagation(); event.currentTarget.classList.add("drag-over"); }
function handleDragLeave(event) { event.preventDefault(); event.stopPropagation(); event.currentTarget.classList.remove("drag-over"); }
function handleDrop(event) { event.preventDefault(); event.stopPropagation(); event.currentTarget.classList.remove("drag-over");
const files = event.dataTransfer.files; if (files.length > 0) { processZipFile(files[0]); } }
function triggerFileInput() { document.getElementById("file-input").click(); }
function handleFileSelect(event) { const files = event.target.files; if (files.length > 0) { processZipFile(files[0]); } // Reset input so the same file can be selected again event.target.value = ""; }
async function processZipFile(file) { if (!file.name.toLowerCase().endsWith(".zip")) { state.result = { success: false, message: `Invalid file type: ${file.name}. Please select a .zip file.`, }; renderResult(); return; }
if (typeof JSZip === "undefined") { state.result = { success: false, message: "JSZip library failed to load. Check your internet connection.", }; renderResult(); return; }
// Show loading state const dropZone = document.getElementById("drop-zone"); if (dropZone) { dropZone.innerHTML = ` <div class="drop-zone-icon"><span class="loading-spinner"></span></div> <div class="drop-zone-text">Processing ${esc(file.name)}...</div> `; }
try { const zip = await JSZip.loadAsync(file); const jsonFiles = [];
// Find all .json files in the zip (including nested folders) // Skip macOS metadata files (__MACOSX) and hidden files (._*) zip.forEach((relativePath, zipEntry) => { if ( !zipEntry.dir && relativePath.toLowerCase().endsWith(".json") && !relativePath.startsWith("__MACOSX/") && !relativePath.split("/").some((part) => part.startsWith("._")) ) { jsonFiles.push({ path: relativePath, entry: zipEntry }); } });
if (jsonFiles.length === 0) { state.result = { success: false, message: `No .json files found in ${file.name}`, }; renderLexiconsSection(); renderResult(); return; }
// Extract and parse all JSON files const lexicons = []; const errors = [];
for (const { path, entry } of jsonFiles) { try { const content = await entry.async("string"); // Validate it's valid JSON JSON.parse(content); lexicons.push({ path, content }); } catch (e) { errors.push(`${path}: ${e.message}`); } }
if (lexicons.length === 0) { state.result = { success: false, message: `All JSON files had parse errors:\n${errors.join("\n")}`, }; renderLexiconsSection(); renderResult(); return; }
// Replace current lexicons with extracted ones (collapsed by default) state.lexicons = lexicons.map((lex, idx) => ({ id: state.nextLexiconId + idx, value: lex.content, collapsed: true, })); state.nextLexiconId += lexicons.length;
const successMsg = `Loaded ${lexicons.length} lexicon${lexicons.length === 1 ? "" : "s"} from ${file.name}`; const errorMsg = errors.length > 0 ? `\n\nSkipped ${errors.length} file${errors.length === 1 ? "" : "s"}:\n${errors.join("\n")}` : "";
state.result = { success: true, message: successMsg + errorMsg, };
renderLexiconsSection(); updateAvailableNsids(); renderResult(); } catch (e) { state.result = { success: false, message: `Failed to read zip file: ${e.message}`, }; renderLexiconsSection(); renderResult(); } }
// ============================================================================= // INIT // =============================================================================
function init() { if (typeof honk === "undefined") { document.getElementById("results-section").innerHTML = ` <div class="result result-error">✗ Failed to load honk library from CDN. Check your internet connection.</div> `; return; } renderLexiconsSection(); renderRecordSection(); }
window.addEventListener("DOMContentLoaded", init); </script> </body></html>