export-app.ts (4856B)
1 import { treeFormat } from './tree-export.js'; 2 import type { BundleHash } from './bundle.js'; 3 import type { ClientMessage, WorkerMessage } from './protocol.js'; 4 type Exported = Extract<WorkerMessage, { type: 'exported' }>; 5 type Request = Extract<ClientMessage, { type: 'export' }>; 6 function el<T extends HTMLElement = HTMLElement>(id: string): T { 7 const element = document.getElementById(id); if (!element) throw new Error(`Missing element ${id}`); return element as T; 8 } 9 // Presentation owns its output and invalidation; the package controller owns the 10 // shared selection and worker. No arguments or execution settings enter exports. 11 export function createTreeExport(request: (message: Request) => void, select: (name: string) => void, stop: () => void) { 12 const definition = el<HTMLSelectElement>('tree-definition'), format = el<HTMLSelectElement>('tree-format'); 13 const button = el<HTMLButtonElement>('export-tree'), cancel = el<HTMLButtonElement>('stop-tree'); 14 const copy = el<HTMLButtonElement>('copy-tree'), download = el<HTMLButtonElement>('download-tree'); 15 const preview = el('tree-preview'), status = el('tree-provenance'), empty = el('tree-empty'); 16 let exported: Exported | undefined, pending = false; 17 const descriptions = { 18 ternary: 'A single line: 0 is Leaf, 1 introduces a Stem, and 2 introduces a Fork. Shared branches are repeated.', 19 tricu: 'A fully parenthesized tree expression using t. This is generated tree structure, not the original source.', 20 triangle: 'A fully parenthesized tree expression using △. Shared branches are repeated.', 21 dag: 'Shared △ applications with a fixed export named tree. Choose DAG when an expanded tree would be too large.', 22 }; 23 function clear(): void { 24 exported = undefined; pending = false; preview.textContent = ''; preview.hidden = true; empty.hidden = false; 25 status.textContent = 'Export the selected stored definition.'; status.classList.remove('error'); 26 copy.disabled = true; download.disabled = true; 27 } 28 function help(): void { el('tree-format-help').textContent = descriptions[treeFormat(format.value)]; } 29 function begin(): void { clear(); pending = true; status.textContent = 'Preparing definition export…'; } 30 function fail(message: string): void { clear(); status.textContent = message; status.classList.add('error'); } 31 format.onchange = () => { clear(); help(); }; 32 definition.onchange = () => { clear(); select(definition.value); }; 33 button.onclick = () => { 34 try { begin(); request({ type: 'export', export: definition.value, format: treeFormat(format.value) }); } 35 catch (error) { fail(error instanceof Error ? error.message : String(error)); } 36 }; 37 cancel.onclick = stop; 38 copy.onclick = async () => { 39 const value = exported; if (!value) return; 40 try { 41 await navigator.clipboard.writeText(value.file.text); 42 if (exported === value) status.textContent += ' · Copied complete tree.'; 43 } catch { if (exported === value) status.textContent += ' · Clipboard unavailable. Use Download tree.'; } 44 }; 45 download.onclick = () => { 46 if (!exported) return; 47 const url = URL.createObjectURL(new Blob([exported.file.text], { type: 'text/plain;charset=utf-8' })); 48 const anchor = document.createElement('a'); anchor.href = url; 49 const name = exported.export.replace(/[^a-zA-Z0-9_-]/gu, '-').slice(0, 64) || 'tree'; 50 anchor.download = `${name}-${exported.hash.slice(0, 12)}.${exported.file.extension}`; 51 document.body.append(anchor); anchor.click(); anchor.remove(); 52 setTimeout(() => URL.revokeObjectURL(url), 1000); 53 }; 54 help(); 55 return { 56 clear, begin, fail, 57 get pending(): boolean { return pending; }, 58 selection(input: HTMLSelectElement): void { 59 definition.replaceChildren(...[...input.options].map(option => option.cloneNode(true))); 60 definition.value = input.value; 61 }, 62 controls(busy: boolean, loaded: boolean): void { 63 button.disabled = format.disabled = definition.disabled = busy || !loaded; 64 cancel.disabled = !busy; copy.disabled = download.disabled = busy || !exported; 65 }, 66 receive(data: Exported, hash: BundleHash | undefined, name: string): boolean { 67 if (data.hash !== hash || data.export !== name || data.format !== format.value) return false; 68 exported = data; pending = false; 69 preview.textContent = data.file.text.slice(0, 16_384); preview.hidden = false; empty.hidden = true; 70 status.classList.remove('error'); 71 status.textContent = `SHA-256 ${data.hash} · definition ${data.export} · ${data.file.bytes.toLocaleString()} UTF-8 bytes · ${data.file.storedNodes.toLocaleString()} stored nodes` + 72 (preview.textContent.length < data.file.text.length ? ' · Preview: first 16,384 characters. Copy and Download contain the complete tree.' : ' · Complete tree.'); 73 return true; 74 }, 75 }; 76 }