(function (app) { 'use strict'; // load() resolves to the table context the rest of the app renders: // { title?, description?, columns, rows, defaults? } // // Two paths: // // 1. Inline JSON (test seam, and also any host that wants to // pre-render a context server-side): if #table-context parses // to a non-empty object, return it as-is. // // 2. File-backed walk (the real-world path served by zddc-server): // page is at //table.html — fetch /table.yaml, // list every other *.yaml in as a row file (filtering // out table.yaml and form.yaml so they don't appear as rows), // parse each, and assemble the same shape. The whole table // lives in one directory. // // file:// mode without a directory handle is unsupported in v1 — the // walk only runs against http(s). file:// users must either inject an // inline context (tests) or open the page through zddc-server. async function load() { const inline = readInlineContext(); if (inline && Object.keys(inline).length > 0) { return inline; } if (typeof location !== 'undefined' && (location.protocol === 'http:' || location.protocol === 'https:')) { try { const walked = await walkServer(); if (walked) { return walked; } } catch (err) { console.error('[tables] failed to load table from server', err); showStatus('Could not load table: ' + (err && err.message ? err.message : err)); } } return {}; } function readInlineContext() { const el = document.getElementById('table-context'); if (!el) { return null; } try { return JSON.parse(el.textContent || '{}'); } catch (err) { console.error('[tables] failed to parse #table-context', err); return null; } } function showStatus(msg) { const el = document.getElementById('table-status'); if (!el) return; el.textContent = msg; el.hidden = false; } async function walkServer() { const source = window.zddc && window.zddc.source; if (!source) { throw new Error('zddc.source not available'); } const tableName = tableNameFromUrl(location.pathname); if (!tableName) { throw new Error('Unrecognized table URL: ' + location.pathname); } const probe = await source.detectServerRoot(); if (!probe.handle) { throw new Error(probe.status === 403 ? 'No permission to list this directory' : 'Server unreachable'); } const dir = probe.handle; // Spec lives at /table.yaml — the page URL is // /table.html, so the spec is right next door. const spec = await readYaml(dir, 'table.yaml'); if (!spec || !Array.isArray(spec.columns)) { throw new Error('Spec table.yaml missing columns[]'); } // Optional row schema from /form.yaml — same JSON Schema // the form-mode renderer uses. Phase 2 derives per-cell editor // widgets from it (text/number/date/select/checkbox). // Best-effort: a directory with only table.yaml still renders // as a sortable/filterable table; cells fall back to plain // text inputs without per-property hints. let rowSchema = null; try { const formSpec = await readYaml(dir, 'form.yaml'); if (formSpec && formSpec.schema) { rowSchema = formSpec.schema; } } catch (_) { // form.yaml missing or unreadable; carry on without it. } // Rows are every *.yaml in EXCEPT the spec // (table.yaml) and the row-edit form (form.yaml). They live // in the same directory by design — copying the directory // copies the whole table. const rows = await readRows(dir, '', tableName); return { title: spec.title, description: spec.description, columns: spec.columns, defaults: spec.defaults, rowSchema: rowSchema, rows: rows }; } function tableNameFromUrl(pathname) { // //...//table.html → name is the rows-dir's // basename. const m = String(pathname || '').match(/\/([^\/]+)\/table\.html$/); return m ? m[1] : null; } function stripDotSlash(p) { let out = String(p || ''); if (out.startsWith('./')) out = out.slice(2); if (out.startsWith('/')) out = out.slice(1); if (out.endsWith('/')) out = out.slice(0, -1); return out; } async function readYaml(dir, relPath) { const fileHandle = await resolveFile(dir, relPath); const file = await fileHandle.getFile(); const text = await file.text(); if (!window.jsyaml) { throw new Error('js-yaml not loaded'); } return window.jsyaml.load(text); } // Walk a "/"-separated relative path under dir, returning the // FileSystemFileHandle (or HttpFileHandle) at the leaf. async function resolveFile(dir, relPath) { const parts = relPath.split('/').filter(Boolean); if (parts.length === 0) { throw new Error('Empty file path'); } const fileName = parts.pop(); let cur = dir; for (let i = 0; i < parts.length; i++) { cur = await cur.getDirectoryHandle(parts[i]); } return cur.getFileHandle(fileName); } async function resolveDirectory(dir, relPath) { const parts = relPath.split('/').filter(Boolean); let cur = dir; for (let i = 0; i < parts.length; i++) { cur = await cur.getDirectoryHandle(parts[i]); } return cur; } async function readRows(rowsDir, _rowsRel, _tableName) { const rows = []; for await (const entry of rowsDir.values()) { if (entry.kind !== 'file') continue; if (!entry.name.endsWith('.yaml')) continue; // Skip the spec and the row-edit form — they live alongside // the rows but aren't rows themselves. if (entry.name === 'table.yaml' || entry.name === 'form.yaml') continue; try { const file = await (await rowsDir.getFileHandle(entry.name)).getFile(); const data = window.jsyaml.load(await file.text()); rows.push({ url: rowEditUrl(entry.name), data: data || {}, editable: true }); } catch (err) { console.warn('[tables] skipping unparseable row', entry.name, err); } } return rows; } // Re-edit URL for one row. Page is at //table.html; row file // lives at //.yaml; form re-edit URL is // //.yaml.html — same directory. function rowEditUrl(rowFileName) { const pageDir = location.pathname.replace(/\/table\.html$/, '/'); return pageDir + rowFileName + '.html'; } app.modules.context = { load: load }; })(window.tablesApp);