tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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 }