import { escapeHtml } from "./utils.js";
function resolveRef(schema, root) {
if (schema && schema.$ref) {
const path = schema.$ref.replace(/^#\//, "").split("/");
let node = root;
for (const seg of path) node = node?.[seg];
return node ? { ...node, ...schema, $ref: undefined } : schema;
}
return schema;
}
function build(schema, root, value, required) {
schema = resolveRef(schema, root) || {};
const desc = schema.description ? `<small class="help">${escapeHtml(firstSentence(schema.description))}</small>` : "";
const variants = schema.oneOf || schema.anyOf;
if (Array.isArray(variants) && variants.every((v) => discriminatorConst(resolveRef(v, root)))) {
return buildDiscriminated(variants, root, value, desc);
}
if (Array.isArray(schema.enum)) return buildEnum(schema, value, desc);
const type = jsonType(schema, value);
if (type === "object" && schema.properties) return buildObject(schema, root, value || {});
if (type === "array") return buildArray(schema, root, value || []);
if (type === "boolean") return buildBool(schema, value, desc);
if (type === "integer" || type === "number") return buildNumber(schema, value, type, desc);
if (type === "string") return buildString(schema, value, desc);
return buildRaw(value); }
function buildObject(schema, root, value) {
const wrap = document.createElement("fieldset");
wrap.className = "sf-object";
const reqset = new Set(schema.required || []);
const readers = {};
for (const [key, sub] of Object.entries(schema.properties)) {
const row = document.createElement("div");
row.className = "sf-field";
const label = document.createElement("label");
label.textContent = key + (reqset.has(key) ? " *" : "");
const ctrl = build(sub, root, value[key], reqset.has(key));
row.appendChild(label);
row.appendChild(ctrl.el);
wrap.appendChild(row);
readers[key] = ctrl.read;
}
return {
el: wrap,
read() {
const out = {};
for (const [k, r] of Object.entries(readers)) {
const v = r();
if (v !== undefined && v !== "" && !(typeof v === "object" && v !== null && Object.keys(v).length === 0))
out[k] = v;
}
return out;
},
};
}
function buildArray(schema, root, value) {
const wrap = document.createElement("div");
wrap.className = "sf-array";
const list = document.createElement("div");
const readers = [];
const addItem = (v) => {
const item = document.createElement("div");
item.className = "sf-array-item";
const ctrl = build(schema.items || {}, root, v, false);
const del = document.createElement("button");
del.type = "button";
del.className = "btn-ghost";
del.textContent = "✕";
del.onclick = () => {
const i = readers.indexOf(ctrl.read);
if (i >= 0) readers.splice(i, 1);
item.remove();
};
item.appendChild(ctrl.el);
item.appendChild(del);
list.appendChild(item);
readers.push(ctrl.read);
};
(Array.isArray(value) ? value : []).forEach(addItem);
const add = document.createElement("button");
add.type = "button";
add.className = "btn-ghost";
add.textContent = "+ add";
add.onclick = () => addItem(undefined);
wrap.appendChild(list);
wrap.appendChild(add);
return { el: wrap, read: () => readers.map((r) => r()).filter((v) => v !== undefined && v !== "") };
}
function buildDiscriminated(variants, root, value, desc) {
const wrap = document.createElement("div");
wrap.className = "sf-union";
const select = document.createElement("select");
const bodies = {};
let current = null;
const render = (tag) => {
if (current) current.el.remove();
const variant = variants.map((v) => resolveRef(v, root)).find((v) => discriminatorConst(v) === tag);
const sub = stripConst(variant);
current = build(sub, root, value && value.type === tag ? value : {}, false);
bodies[tag] = current;
wrap.appendChild(current.el);
};
for (const v of variants) {
const tag = discriminatorConst(resolveRef(v, root));
const opt = document.createElement("option");
opt.value = tag;
opt.textContent = tag;
select.appendChild(opt);
}
select.value = (value && value.type) || select.options[0]?.value;
select.onchange = () => render(select.value);
wrap.appendChild(select);
if (desc) wrap.insertAdjacentHTML("beforeend", desc);
render(select.value);
return {
el: wrap,
read() {
const tag = select.value;
return { type: tag, ...current.read() };
},
};
}
function buildEnum(schema, value, desc) {
const sel = document.createElement("select");
for (const v of schema.enum) {
const o = document.createElement("option");
o.value = String(v);
o.textContent = String(v);
sel.appendChild(o);
}
if (value !== undefined) sel.value = String(value);
else if (schema.default !== undefined) sel.value = String(schema.default);
return withHelp(sel, desc, () => sel.value);
}
function buildBool(schema, value, desc) {
const cb = document.createElement("input");
cb.type = "checkbox";
cb.checked = value !== undefined ? !!value : !!schema.default;
return withHelp(cb, desc, () => cb.checked);
}
function buildNumber(schema, value, type, desc) {
const inp = document.createElement("input");
inp.type = "number";
if (type === "integer") inp.step = "1";
if (value !== undefined) inp.value = value;
else if (schema.default !== undefined) inp.value = schema.default;
if (schema.description) inp.placeholder = firstSentence(schema.description);
return withHelp(inp, desc, () => (inp.value === "" ? undefined : Number(inp.value)));
}
function buildString(schema, value, desc) {
const inp = document.createElement("input");
inp.type = /password|secret|token|key/i.test(schema.title || "") ? "password" : "text";
if (value !== undefined) inp.value = value;
else if (schema.default !== undefined) inp.value = schema.default;
if (schema.description) inp.placeholder = firstSentence(schema.description);
return withHelp(inp, desc, () => (inp.value === "" ? undefined : inp.value));
}
function buildRaw(value) {
const ta = document.createElement("textarea");
ta.className = "sf-raw";
ta.placeholder = "raw JSON";
if (value !== undefined) ta.value = JSON.stringify(value, null, 2);
return {
el: ta,
read() {
if (!ta.value.trim()) return undefined;
try { return JSON.parse(ta.value); } catch { return ta.value; }
},
};
}
function withHelp(el, desc, read) {
if (!desc) return { el, read };
const wrap = document.createElement("div");
wrap.appendChild(el);
wrap.insertAdjacentHTML("beforeend", desc);
return { el: wrap, read };
}
function jsonType(schema, value) {
if (typeof schema.type === "string") return schema.type;
if (Array.isArray(schema.type)) return schema.type.find((t) => t !== "null") || "string";
if (schema.properties) return "object";
if (schema.items) return "array";
if (schema.enum) return typeof schema.enum[0];
if (value !== undefined) return Array.isArray(value) ? "array" : typeof value;
return "string";
}
function discriminatorConst(variant) {
const p = variant && variant.properties && variant.properties.type;
if (!p) return null;
if (p.const !== undefined) return p.const;
if (Array.isArray(p.enum) && p.enum.length === 1) return p.enum[0];
return null;
}
function stripConst(variant) {
const props = { ...(variant.properties || {}) };
delete props.type;
return { type: "object", properties: props, required: (variant.required || []).filter((r) => r !== "type") };
}
function firstSentence(s) {
const m = s.split(/\.\s/)[0];
return (m.length > 120 ? m.slice(0, 117) + "…" : m).replace(/\.$/, "");
}
export function renderSchemaForm(schema, value) {
return build(schema, schema, value, true);
}