(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);