(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): // fetch /.zddc, find tables[], fetch the *.table.yaml // spec, list //*.yaml row files, parse each, and // assemble the same shape. // // 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; const zddcDoc = await readYaml(dir, '.zddc'); const tablesMap = (zddcDoc && zddcDoc.tables) || {}; const specRel = tablesMap[tableName]; if (!specRel) { throw new Error('No tables.' + tableName + ' declared in .zddc'); } const spec = await readYaml(dir, stripDotSlash(specRel)); if (!spec || !Array.isArray(spec.columns)) { throw new Error('Spec ' + specRel + ' missing columns[]'); } const rowsRel = stripDotSlash(spec.rows || ('./' + tableName)); const rowsDir = await resolveDirectory(dir, rowsRel); const rows = await readRows(rowsDir, rowsRel, tableName); return { title: spec.title, description: spec.description, columns: spec.columns, defaults: spec.defaults, rows: rows }; } function tableNameFromUrl(pathname) { 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; try { const file = await (await rowsDir.getFileHandle(entry.name)).getFile(); const data = window.jsyaml.load(await file.text()); rows.push({ url: rowEditUrl(rowsRel, tableName, entry.name), data: data || {}, editable: true }); } catch (err) { console.warn('[tables] skipping unparseable row', entry.name, err); } } return rows; } // Build the form-handler URL for editing one row. The page is at // /.table.html; the row file lives at // //.yaml; the form re-edit URL is // //.yaml.html. function rowEditUrl(rowsRel, tableName, rowFileName) { const pageDir = location.pathname.replace(/\/[^\/]+\.table\.html$/, '/'); const rowsPath = pageDir + (rowsRel || tableName) + '/'; return rowsPath + rowFileName + '.html'; } app.modules.context = { load: load }; })(window.tablesApp);