document.addEventListener('alpine:init', () => {
Alpine.data('explorer', () => ({
schema: null,
defs: {},
topLevel: [],
selected: null,
selectedAnchor: null,
filter: '',
searchIndex: null,
tree: [],
drawerOpen: false,
activeTab: 'settings',
searchOpen: false,
_allItems: [],
init() {
const bundle = window.ZENOPS_SCHEMA;
if (!bundle) {
this.$el.innerHTML =
'<p>Schema not loaded. Re-run <code>just docs-build</code>.</p>';
return;
}
this.schema = bundle.schemas.config;
this.defs = this.schema.$defs || {};
this.topLevel = Object.keys(this.schema.properties || {});
this._allItems = this._buildAllItems();
this._buildSearchIndex();
this.tree = buildTree(this.schema);
document.body.classList.add('zo-explorer-page');
window.addEventListener('hashchange', () => this._applyHash());
this.$watch('filter', (q) => {
if (q && q.trim()) this.drawerOpen = true;
});
this._wrapChapterIntro();
this.$watch('selected', () => this._syncChapterIntro());
this._applyHash();
this._syncChapterIntro();
},
_wrapChapterIntro() {
const intro = [];
let prev = this.$el.previousElementSibling;
while (prev) {
if (prev.id && prev.id.startsWith('mdbook')) break;
intro.unshift(prev);
prev = prev.previousElementSibling;
}
this._chapterIntro = intro;
},
_syncChapterIntro() {
if (!this._chapterIntro) return;
const hide = !!this.selected;
for (const node of this._chapterIntro) {
node.style.display = hide ? 'none' : '';
}
},
_buildAllItems() {
const sections = this.topLevel.map((name) => ({
id: name, name, kind: 'section', kindLabel: 'section', description: '',
}));
const defs = Object.entries(this.defs).map(([name, def]) => ({
id: name, name, kind: defKindOf(def), kindLabel: defKindOf(def),
description: def.description || '',
}));
defs.sort((a, b) => a.name.localeCompare(b.name));
return [...sections, ...defs];
},
_buildSearchIndex() {
this.searchIndex = new window.MiniSearch({
fields: ['name', 'description', 'fields'],
storeFields: ['name', 'kind', 'kindLabel'],
searchOptions: { boost: { name: 4, fields: 2 }, fuzzy: 0.2, prefix: true },
});
const docs = this._allItems.map((item) => {
const def = item.kind === 'section' ? null : this.defs[item.name];
return {
id: item.id, name: item.name, kind: item.kind, kindLabel: item.kindLabel,
description: item.description,
fields: def ? collectFieldNames(def).join(' ') : '',
};
});
this.searchIndex.addAll(docs);
},
_applyHash() {
const hash = window.location.hash;
if (!hash.startsWith('#/')) return;
const rest = hash.slice(2);
const [typePart, anchor] = rest.split('$');
this.selected = (typePart || '').trim() || this.topLevel[0] || null;
this.selectedAnchor = anchor || null;
this.drawerOpen = false;
if (anchor) {
requestAnimationFrame(() => {
const el = document.getElementById(anchor);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
}
},
get displayList() {
if (!this.filter.trim()) {
const sections = this._allItems.filter((i) => i.kind === 'section');
const defs = this._allItems.filter((i) => i.kind !== 'section');
return [
{ kind: 'heading', label: 'Top-level sections', key: 'h-sections' },
...sections.map((i) => ({ ...i, key: 's-' + i.id })),
{ kind: 'heading', label: 'All types', key: 'h-defs' },
...defs.map((i) => ({ ...i, key: 'd-' + i.id })),
];
}
const results = this.searchIndex.search(this.filter);
return results.map((r) => ({
id: r.id, name: r.name, kind: r.kind, kindLabel: r.kindLabel, key: 'r-' + r.id,
}));
},
renderSelected() {
if (!this.selected) return welcome(this);
if (this.topLevel.includes(this.selected)) return renderSection(this, this.selected);
if (this.defs[this.selected]) return renderDef(this, this.selected, this.defs[this.selected]);
return `<div class="se-empty">Unknown: <code>${esc(this.selected)}</code></div>`;
},
renderSidebar() {
if (this.activeTab === 'pages') return renderPagesTab();
if (this.filter && this.filter.trim()) {
const list = this.displayList;
if (list.length === 0) return `<div class="se-nav-empty">No matches</div>`;
return list.filter((i) => i.kind !== 'heading').map((i) =>
`<a class="se-tree-row${i.id === this.selected ? ' se-selected' : ''}" href="#/${esc(i.id)}">
<span class="se-tree-toggle se-leaf">·</span>
<span class="se-tree-name">${esc(i.name)}</span>
<span class="se-tree-kind">${esc(i.kindLabel)}</span>
</a>`
).join('');
}
const selectedKey = this.selectedAnchor
? `${this.selected}$${this.selectedAnchor}`
: this.selected;
const treeNodes = this.tree.filter((n) => n.kind !== 'heading');
const expandIds = findAncestors(treeNodes, selectedKey);
const rootSection = findRootSection(treeNodes, selectedKey);
if (rootSection) expandIds.add(rootSection);
for (const node of treeNodes) {
if (node.kind === 'bucket') expandIds.add(node.id);
}
return this.tree.map((node) => {
if (node.kind === 'heading') {
return `<div class="se-tree-section-heading">${esc(node.label)}</div>`;
}
return renderTreeNode(node, 0, selectedKey, expandIds);
}).join('');
},
}));
});
const BOOK_TOPICS = [
{ url: '../getting-started.html', label: 'Introduction' },
{ url: 'config.html', label: 'Configuration file', current: true },
{ url: 'cli.html', label: 'Command-line interface' },
];
function renderPagesTab() {
return `<div class="se-nav-heading">Book topics</div>` +
BOOK_TOPICS.map((t) =>
`<a class="se-tree-row${t.current ? ' se-selected' : ''}" href="${esc(t.url)}">
<span class="se-tree-toggle se-leaf">·</span>
<span class="se-tree-name">${esc(t.label)}</span>
</a>`
).join('');
}
function esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function refName(ref) { return ref.replace(/^#\/\$defs\//, ''); }
function anchorId(prefix, key) {
return prefix + '-' + String(key).replace(/[^A-Za-z0-9_]/g, '_');
}
function defKindOf(def) {
if (!def) return 'type';
if (def.oneOf) return 'enum';
if (def.type === 'object') return 'object';
if (def.type === 'string' && def.enum) return 'enum';
if (def.type) return def.type;
return 'type';
}
function unwrapNullable(def) {
if (!def) return null;
if (Array.isArray(def.anyOf) && def.anyOf.length === 2) {
const [a, b] = def.anyOf;
if (a && a.type === 'null') return b;
if (b && b.type === 'null') return a;
}
if (Array.isArray(def.type) && def.type.includes('null') && def.type.length === 2) {
const inner = def.type.find((t) => t !== 'null');
return { ...def, type: inner };
}
return null;
}
function mdInline(s) {
if (s == null) return '';
const links = [];
const withoutLinks = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
const placeholder = ` L${links.length} `;
links.push({ text, url });
return placeholder;
});
let html = esc(withoutLinks).replace(/`([^`]+)`/g, (_, code) => `<code>${code}</code>`);
html = html.replace(/ L(\d+) /g, (_, i) => {
const { text, url } = links[+i];
const inner = esc(text).replace(/`([^`]+)`/g, (_m, code) => `<code>${code}</code>`);
return `<a href="${esc(url)}">${inner}</a>`;
});
return html;
}
function mdBlock(s) {
if (s == null) return '';
const parts = [];
const fence = /```([a-zA-Z0-9_-]*)\n([\s\S]*?)```/g;
let last = 0;
let m;
while ((m = fence.exec(s)) !== null) {
if (m.index > last) parts.push({ kind: 'prose', text: s.slice(last, m.index) });
parts.push({ kind: 'code', lang: m[1], text: m[2] });
last = m.index + m[0].length;
}
if (last < s.length) parts.push({ kind: 'prose', text: s.slice(last) });
return parts.map((part) => {
if (part.kind === 'code') {
const body = part.text.replace(/\n$/, '');
if (part.lang === 'toml') {
return `<pre class="se-code-block se-code-block-toml">${highlightToml(body)}</pre>`;
}
const langAttr = part.lang ? ` class="language-${esc(part.lang)}"` : '';
return `<pre class="se-code-block"><code${langAttr}>${esc(body)}</code></pre>`;
}
return part.text.split(/\n[ \t]*\n/).map((para) => {
para = para.trim();
if (!para) return '';
const lines = para.split(/\n/);
if (lines.every((l) => /^\s*-\s+/.test(l))) {
const items = lines.map((l) => `<li>${mdInline(l.replace(/^\s*-\s+/, ''))}</li>`).join('');
return `<ul>${items}</ul>`;
}
return `<p>${mdInline(lines.join(' '))}</p>`;
}).join('');
}).join('');
}
function collectFieldNames(def) {
const names = new Set();
const visit = (node) => {
if (!node || typeof node !== 'object') return;
if (node.properties) for (const k of Object.keys(node.properties)) names.add(k);
if (node.oneOf) for (const v of node.oneOf) visit(v);
};
visit(def);
return [...names];
}
function variantLabel(variant) {
if (!variant.properties) {
if (variant.const !== undefined) return JSON.stringify(variant.const);
return null;
}
for (const [k, v] of Object.entries(variant.properties)) {
if (v.const !== undefined) return `${k} = ${JSON.stringify(v.const)}`;
}
const required = variant.required || Object.keys(variant.properties);
const nonDiscriminator = required.filter((k) => variant.properties[k].const === undefined);
if (nonDiscriminator.length === 1) return nonDiscriminator[0];
if (required.length === 1) return required[0];
return null;
}
function variantShortName(variant) {
if (!variant.properties) {
if (variant.const !== undefined) return JSON.stringify(variant.const);
return null;
}
for (const v of Object.values(variant.properties)) {
if (v.const !== undefined) return String(v.const);
}
const required = variant.required || Object.keys(variant.properties);
const nonDiscriminator = required.filter((k) => variant.properties[k].const === undefined);
if (nonDiscriminator.length === 1) return nonDiscriminator[0];
if (required.length === 1) return required[0];
return null;
}
function renderTypeInline(field) {
if (!field) return '';
const opt = unwrapNullable(field);
if (opt) return renderTypeInline(opt);
if (field.$ref) {
const n = refName(field.$ref);
return `<a href="#/${esc(n)}"><code>${esc(n)}</code></a>`;
}
if (field.type === 'array') return `array of ${renderTypeInline(field.items || {})}`;
if (field.type === 'object') {
if (field.additionalProperties) return `map of ${renderTypeInline(field.additionalProperties)}`;
return 'object';
}
if (field.oneOf) {
const allStringConsts = field.oneOf.every((v) => v.type === 'string' && (v.const !== undefined || v.enum));
if (allStringConsts) {
const vals = field.oneOf.flatMap((v) => v.const !== undefined ? [v.const] : (v.enum || []));
return vals.map((v) => `<code>"${esc(v)}"</code>`).join(' | ');
}
return `oneOf (${field.oneOf.length})`;
}
if (field.enum) return field.enum.map((v) => `<code>${esc(JSON.stringify(v))}</code>`).join(' | ');
if (field.type) return `<code>${esc(field.type)}</code>`;
return '<code>unknown</code>';
}
function renderSignature(typeName, def) {
if (def.oneOf) {
const variants = def.oneOf
.map((v, i) => variantShortName(v) || `variant${i + 1}`)
.map((n) => `<div class="se-sig-variant"><a href="#/${esc(typeName)}$${anchorId('variant', n)}">${esc(n)}</a><span class="se-sig-punct">,</span></div>`)
.join('');
return `<div class="se-signature"><span class="se-sig-keyword">enum</span> <span class="se-sig-name">${esc(typeName)}</span> <span class="se-sig-punct">{</span>
${variants}<span class="se-sig-punct">}</span></div>`;
}
if (def.type === 'object' && def.properties && Object.keys(def.properties).length) {
const fields = Object.keys(def.properties)
.map((f) => `<div class="se-sig-field"><a href="#/${esc(typeName)}$${anchorId('field', f)}">${esc(f)}</a><span class="se-sig-punct">,</span></div>`)
.join('');
return `<div class="se-signature"><span class="se-sig-keyword">struct</span> <span class="se-sig-name">${esc(typeName)}</span> <span class="se-sig-punct">{</span>
${fields}<span class="se-sig-punct">}</span></div>`;
}
if (def.type === 'string' && def.enum) {
const values = def.enum.map((v) => `<span class="se-sig-variant" style="padding-left:0"><a href="#/${esc(typeName)}$${anchorId('value', v)}">"${esc(v)}"</a></span>`).join(' <span class="se-sig-punct">|</span> ');
return `<div class="se-signature"><span class="se-sig-keyword">type</span> <span class="se-sig-name">${esc(typeName)}</span> <span class="se-sig-punct">=</span> ${values}</div>`;
}
return '';
}
const TOML_HINTS = {
'StoredGitSigning.ssh.key': '"~/.ssh/id_ed25519-github.pub"',
'StoredGitSigning.gpg.key': '"3AA5C34371567BD2"',
};
function placeholderFor(typeName, variantTag, fieldName, field, defs) {
const key = variantTag ? `${typeName}.${variantTag}.${fieldName}` : `${typeName}.${fieldName}`;
if (TOML_HINTS[key]) return TOML_HINTS[key];
const unwrapped = unwrapNullable(field) || field;
if (unwrapped.const !== undefined) return JSON.stringify(unwrapped.const);
if (unwrapped.$ref) {
const t = refName(unwrapped.$ref);
const target = defs[t];
if (target) {
if (target.type === 'string' && target.enum) return `"${target.enum[0]}"`;
if (target.type === 'string') {
if (t === 'ExpandStr') return `"${'$'}{value}"`;
if (t === 'SafeRelativePath' || t === 'StoredRelativePath') return `"path/to/file"`;
if (t === 'HostnameRegex') return `"^hostname-pattern$"`;
if (t === 'SinglePathComponent') return `"name"`;
if (t === 'Shell') return `"bash"`;
return `""`;
}
return `{ # ${t}\n # …\n}`;
}
return `{ # ${t} }`;
}
if (unwrapped.type === 'string') {
if (unwrapped.enum) return `"${unwrapped.enum[0]}"`;
return `""`;
}
if (unwrapped.type === 'boolean') return unwrapped.default !== undefined ? String(unwrapped.default) : 'false';
if (unwrapped.type === 'integer' || unwrapped.type === 'number') return '0';
if (unwrapped.type === 'array') return '[]';
if (unwrapped.type === 'object') return '{ }';
return '# …';
}
function highlightToml(text) {
return text.split('\n').map((line) => {
const m1 = line.match(/^(\s*)(\[[^\]]+\])(.*)$/);
if (m1) return `${esc(m1[1])}<span class="se-toml-section">${esc(m1[2])}</span>${esc(m1[3])}`;
const m2 = line.match(/^(\s*)([a-zA-Z_][a-zA-Z0-9_.-]*)(\s*=\s*)(.*)$/);
if (m2) {
const value = m2[4];
const valueHtml = value.replace(/"([^"]*)"/g, (_, s) => `<span class="se-toml-string">"${esc(s)}"</span>`);
const commentMatch = valueHtml.match(/^(.*?)(\s+#.*)$/);
if (commentMatch) {
return `${esc(m2[1])}<span class="se-toml-key">${esc(m2[2])}</span>${esc(m2[3])}${commentMatch[1]}<span class="se-toml-comment">${esc(commentMatch[2])}</span>`;
}
return `${esc(m2[1])}<span class="se-toml-key">${esc(m2[2])}</span>${esc(m2[3])}${valueHtml}`;
}
const m3 = line.match(/^(\s*)(#.*)$/);
if (m3) return `${esc(m3[1])}<span class="se-toml-comment">${esc(m3[2])}</span>`;
return esc(line);
}).join('\n');
}
function tomlPathFor(typeName, schema) {
const defs = schema.$defs || {};
const queue = [];
for (const sec of Object.keys(schema.properties || {})) {
const prop = schema.properties[sec];
if (prop.$ref && refName(prop.$ref) === typeName) return [sec];
if (prop.additionalProperties && prop.additionalProperties.$ref) {
if (refName(prop.additionalProperties.$ref) === typeName) return [sec, '<name>'];
queue.push({ type: refName(prop.additionalProperties.$ref), path: [sec, '<name>'] });
}
if (prop.$ref) queue.push({ type: refName(prop.$ref), path: [sec] });
}
const visited = new Set();
while (queue.length) {
const { type, path } = queue.shift();
if (visited.has(type)) continue;
visited.add(type);
const def = defs[type];
if (!def) continue;
const stepInto = (field, fdef) => {
const unwrapped = unwrapNullable(fdef) || fdef;
if (unwrapped.$ref) {
const t = refName(unwrapped.$ref);
if (t === typeName) return [...path, field];
queue.push({ type: t, path: [...path, field] });
} else if (unwrapped.additionalProperties && unwrapped.additionalProperties.$ref) {
const t = refName(unwrapped.additionalProperties.$ref);
if (t === typeName) return [...path, field, '<name>'];
queue.push({ type: t, path: [...path, field, '<name>'] });
}
return null;
};
if (def.properties) {
for (const [f, fd] of Object.entries(def.properties)) {
const found = stepInto(f, fd);
if (found) return found;
}
}
if (def.oneOf) {
for (const variant of def.oneOf) {
if (variant.properties) {
for (const [f, fd] of Object.entries(variant.properties)) {
const found = stepInto(f, fd);
if (found) return found;
}
}
}
}
}
return null;
}
let exampleSeq = 0;
function tomlBlockHtml(label, body) {
const id = 'se-ex-' + (++exampleSeq);
return `
<div class="se-example">
<div class="se-example-head">
<span class="se-example-label">${label}</span>
<button class="se-example-copy" onclick="navigator.clipboard?.writeText(document.getElementById('${id}').innerText)">Copy</button>
</div>
<pre id="${id}">${highlightToml(body)}</pre>
</div>
`;
}
function renderTomlExampleForObject(typeName, def, schema) {
const path = tomlPathFor(typeName, schema);
if (!path || !def.properties) return '';
const header = '[' + path.join('.') + ']';
const defs = schema.$defs || {};
const lines = [header];
for (const [field, fdef] of Object.entries(def.properties)) {
lines.push(`${field} = ${placeholderFor(typeName, null, field, fdef, defs)}`);
}
return `<h2>TOML example</h2><div class="se-examples-block">${tomlBlockHtml(`<strong>${esc(typeName)}</strong>`, lines.join('\n'))}</div>`;
}
function renderTomlExampleForVariant(typeName, variant, schema) {
if (Array.isArray(variant.examples) && variant.examples.length > 0) {
const body = String(variant.examples[0]);
return `<div class="se-examples-block" style="margin: 0.5rem 0 0.75rem;">${tomlBlockHtml('Example', body)}</div>`;
}
const path = tomlPathFor(typeName, schema);
if (!path || !variant.properties) return '';
const header = '[' + path.join('.') + ']';
const defs = schema.$defs || {};
let tag = null;
for (const v of Object.values(variant.properties)) if (v.const !== undefined) tag = v.const;
const entries = Object.entries(variant.properties).sort(([a], [b]) => {
const aTag = variant.properties[a].const !== undefined ? -1 : 0;
const bTag = variant.properties[b].const !== undefined ? -1 : 0;
return aTag - bTag;
});
const lines = [header];
for (const [field, fdef] of entries) {
lines.push(`${field} = ${placeholderFor(typeName, tag, field, fdef, defs)}`);
}
return `<div class="se-examples-block" style="margin: 0.5rem 0 0.75rem;">${tomlBlockHtml('Example', lines.join('\n'))}</div>`;
}
function welcome(ctx) {
const topLevel = ctx.topLevel;
const sections = topLevel.filter((n) => !RULE_SECTIONS.has(n));
const rules = topLevel.filter((n) => RULE_SECTIONS.has(n));
const summaryFor = (name) => {
const prop = ctx.schema.properties[name];
let desc = (prop && prop.description) || '';
if (!desc) {
let target = null;
if (prop && prop.$ref) target = refName(prop.$ref);
else if (prop && prop.additionalProperties && prop.additionalProperties.$ref) {
target = refName(prop.additionalProperties.$ref);
}
desc = (target && ctx.defs[target] && ctx.defs[target].description) || '';
}
if (!desc) return '';
return desc.split(/\n[ \t]*\n/)[0].replace(/\n/g, ' ').trim();
};
const groupHtml = (heading, names) => {
if (!names.length) return '';
return `
<section class="se-landing-group">
<h2>${esc(heading)}</h2>
<dl class="se-landing-list">
${names.map((n) => {
const summary = summaryFor(n);
return `
<dt><a href="#/${esc(n)}"><code>${esc(n)}</code></a></dt>
<dd>${summary ? mdInline(summary) : '<span class="se-desc-meta">No description yet.</span>'}</dd>
`;
}).join('')}
</dl>
</section>`;
};
return `<div class="se-welcome">
${groupHtml('Sections', sections)}
${groupHtml('Reusable rules', rules)}
<section class="se-landing-group">
<h2>Types</h2>
<p class="se-desc-meta">Every type the schema defines is listed in the sidebar under <strong>Types</strong>, grouped by enum, object, and primitive.</p>
</section>
</div>`;
}
function renderDef(ctx, name, def) {
const kind = defKindOf(def);
const klass = kind === 'object' ? 'object' : kind === 'enum' ? 'enum' : 'primitive';
const topExample = def.oneOf ? '' : renderTomlExampleForObject(name, def, ctx.schema);
return `
<header class="se-header">
<h1><span class="se-type-name">${esc(name)}</span><span class="se-kind-badge se-kind-${klass}">${esc(kind)}</span></h1>
</header>
${renderSignature(name, def)}
${def.description ? `<div class="se-desc">${mdBlock(def.description)}</div>` : ''}
${topExample}
${renderBody(ctx, def, name)}
`;
}
function renderSection(ctx, name) {
const prop = ctx.schema.properties[name];
let body = '';
if (prop.$ref) {
const target = refName(prop.$ref);
const targetDef = ctx.defs[target] || {};
const desc = targetDef.description ? `<div class="se-desc">${mdBlock(targetDef.description)}</div>` : '';
body = desc +
`<p class="se-desc se-desc-meta">Section <code>[${esc(name)}]</code> is a <a href="#/${esc(target)}"><code>${esc(target)}</code></a>.</p>` +
renderBody(ctx, targetDef, target);
} else if (prop.type === 'object' && prop.additionalProperties) {
const inner = renderTypeInline(prop.additionalProperties);
body = `<p class="se-desc se-desc-meta">A table of named entries; each value is ${inner}.</p>`;
const target = prop.additionalProperties.$ref ? refName(prop.additionalProperties.$ref) : null;
if (target && ctx.defs[target]) {
const targetDef = ctx.defs[target];
if (targetDef.description) body = `<div class="se-desc">${mdBlock(targetDef.description)}</div>` + body;
body += `<h2>Entry shape: ${esc(target)}</h2>` + renderBody(ctx, targetDef, target);
}
} else {
body = renderBody(ctx, prop, name);
}
return `
<header class="se-header">
<h1><span class="se-type-name">[${esc(name)}]</span><span class="se-kind-badge">section</span></h1>
</header>
${prop.description ? `<div class="se-desc">${mdBlock(prop.description)}</div>` : ''}
${body}
`;
}
function renderBody(ctx, def, typeName) {
const unwrapped = unwrapNullable(def) || def;
if (unwrapped.oneOf) return renderOneOf(ctx, unwrapped, typeName);
if (unwrapped.type === 'object' && unwrapped.properties && Object.keys(unwrapped.properties).length) return renderFields(unwrapped);
if (unwrapped.type) return renderPrim(unwrapped);
return '';
}
function renderFields(def) {
const required = new Set(def.required || []);
const rows = Object.entries(def.properties).map(([name, field]) => {
const isReq = required.has(name);
const typeHtml = renderTypeInline(field);
const def_ = field.default;
const defaultHtml = def_ !== undefined ? `<code>${esc(JSON.stringify(def_))}</code>` : `<span class="se-muted">—</span>`;
const descHtml = field.description ? `<tr class="se-desc-row"><td colspan="3">${mdBlock(field.description)}</td></tr>` : '';
const fid = anchorId('field', name);
return `
<tr class="se-fld" id="${fid}">
<td class="se-field-name"><code>${esc(name)}</code>${isReq ? '<span class="se-req-badge">required</span>' : ''}</td>
<td class="se-field-type">${typeHtml}</td>
<td class="se-field-default">${defaultHtml}</td>
</tr>${descHtml}`;
}).join('');
return `<h2>Fields</h2>
<table class="se-fields">
<thead><tr><th>Field</th><th>Type</th><th>Default</th></tr></thead>
<tbody>${rows}</tbody>
</table>`;
}
function renderOneOf(ctx, def, typeName) {
const allStringConsts = def.oneOf.every((v) => v.type === 'string' && (v.const !== undefined || v.enum));
if (allStringConsts) {
const rows = def.oneOf.map((v) => {
const values = v.const !== undefined ? [v.const] : (v.enum || []);
const valHtml = values.map((x) => `<code>"${esc(x)}"</code>`).join(', ');
const first = values[0];
const vid = first !== undefined ? anchorId('variant', first) : '';
return `
<tr class="se-fld"${vid ? ` id="${vid}"` : ''}><td>${valHtml}</td><td></td><td></td></tr>
${v.description ? `<tr class="se-desc-row"><td colspan="3">${mdBlock(v.description)}</td></tr>` : ''}`;
}).join('');
return `<h2>Values</h2>
<table class="se-fields"><thead><tr><th>Value</th><th></th><th></th></tr></thead><tbody>${rows}</tbody></table>`;
}
const variants = def.oneOf.map((variant, idx) => {
const label = variantLabel(variant) || `variant ${idx + 1}`;
const shortName = variantShortName(variant) || `variant${idx + 1}`;
const vid = anchorId('variant', shortName);
const example = typeName ? renderTomlExampleForVariant(typeName, variant, ctx.schema) : '';
return `
<section class="se-variant" id="${vid}">
<h3>${esc(label)}</h3>
${variant.description ? `<div class="se-desc" style="margin: 0.3rem 0 0.6rem;">${mdBlock(variant.description)}</div>` : ''}
${example}
${renderBody({ defs: {}, schema: ctx.schema }, variant)}
</section>`;
}).join('');
return `<h2>Variants <span class="se-muted" style="font-weight: 500; text-transform: none; letter-spacing: 0;">— one of ${def.oneOf.length}</span></h2>${variants}`;
}
function renderPrim(def) {
const parts = [`<code>${esc(Array.isArray(def.type) ? def.type.join(' | ') : def.type)}</code>`];
if (def.format) parts.push(`<span class="se-muted">(format: ${esc(def.format)})</span>`);
if (def.pattern) parts.push(`<span class="se-muted">(pattern: <code>${esc(def.pattern)}</code>)</span>`);
if (def.enum) parts.push(' · one of ' + def.enum.map((x) => `<code>"${esc(x)}"</code>`).join(', '));
return `<div class="se-desc">${parts.join(' ')}</div>`;
}
function collectRefs(typeName, defs, ancestors) {
const def = defs[typeName];
if (!def) return [];
const directRefs = new Set();
const walk = (node) => {
if (!node || typeof node !== 'object') return;
if (node.$ref) {
const r = refName(node.$ref);
if (defs[r]) directRefs.add(r);
}
for (const k of Object.keys(node)) { if (k === '$ref') continue; walk(node[k]); }
};
walk(def);
const children = [];
for (const r of directRefs) {
if (ancestors.has(r)) continue;
const newA = new Set(ancestors); newA.add(r);
children.push({ id: r, name: r, kind: defKindOf(defs[r]), children: collectRefs(r, defs, newA) });
}
children.sort((a, b) => a.name.localeCompare(b.name));
return children;
}
const RULE_SECTIONS = new Set(['conditions']);
function buildTree(schema) {
const defs = schema.$defs || {};
const props = schema.properties || {};
const sections = Object.keys(props)
.filter((name) => !RULE_SECTIONS.has(name))
.map((name) => buildSectionNode(name, props[name], defs));
const rules = Object.keys(props)
.filter((name) => RULE_SECTIONS.has(name))
.map((name) => buildSectionNode(name, props[name], defs));
const typeBuckets = bucketTypes(defs);
return [
{ kind: 'heading', label: 'Sections' },
...sections,
{ kind: 'heading', label: 'Reusable rules' },
...rules,
{ kind: 'heading', label: 'Types' },
...typeBuckets,
];
}
function buildSectionNode(name, prop, defs) {
const node = { id: name, name, kind: 'section', children: [] };
let target = null;
if (prop && prop.$ref) target = refName(prop.$ref);
else if (prop && prop.additionalProperties && prop.additionalProperties.$ref) {
target = refName(prop.additionalProperties.$ref);
}
if (!target || !defs[target]) return node;
const def = defs[target];
if (def.type === 'object' && def.properties) {
node.children = Object.keys(def.properties).map((field) => ({
id: `${target}$${anchorId('field', field)}`,
name: field,
kind: 'field',
children: [],
}));
return node;
}
node.children = [{
id: target,
name: target,
kind: defKindOf(def),
children: [],
}];
return node;
}
function bucketTypes(defs) {
const entries = Object.entries(defs).map(([name, def]) => ({
id: name, name, kind: defKindOf(def), children: [],
}));
const bucket = (label, kinds) => {
const matches = entries
.filter((e) => kinds.includes(e.kind))
.sort((a, b) => a.name.localeCompare(b.name));
if (!matches.length) return null;
return {
id: '__bucket_' + label.toLowerCase(),
name: label,
kind: 'bucket',
children: matches,
};
};
return [
bucket('Enums', ['enum']),
bucket('Objects', ['object']),
bucket('Primitives', ['string', 'integer', 'number', 'boolean', 'type']),
].filter(Boolean);
}
function findAncestors(roots, target) {
const ancestors = new Set();
const dfs = (node, path) => {
if (node.id === target) { for (const id of path) ancestors.add(id); return; }
for (const child of node.children || []) dfs(child, [...path, node.id]);
};
for (const root of roots) dfs(root, []);
return ancestors;
}
function findRootSection(roots, target) {
for (const root of roots) {
const dfs = (n) => n.id === target || (n.children || []).some(dfs);
if (dfs(root)) return root.id;
}
return null;
}
const SVG_ICON_ATTRS = 'viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"';
const SECTION_ICONS = {
shell: `<svg ${SVG_ICON_ATTRS}><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>`,
pkg: `<svg ${SVG_ICON_ATTRS}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>`,
ssh: `<svg ${SVG_ICON_ATTRS}><path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"></path><path d="m21 2-9.6 9.6"></path><circle cx="7.5" cy="15.5" r="5.5"></circle></svg>`,
user: `<svg ${SVG_ICON_ATTRS}><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`,
git: `<svg ${SVG_ICON_ATTRS}><line x1="6" y1="3" x2="6" y2="15"></line><circle cx="18" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><path d="M18 9a9 9 0 0 1-9 9"></path></svg>`,
conditions: `<svg ${SVG_ICON_ATTRS}><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>`,
};
function renderTreeNode(node, depth, selected, autoExpand) {
const isSelected = node.id === selected;
const hasChildren = node.children && node.children.length > 0;
const isExpanded = hasChildren && autoExpand.has(node.id);
const toggle = hasChildren ? (isExpanded ? '▾' : '▸') : '·';
const skipKind = node.kind === 'section' || node.kind === 'field' || node.kind === 'bucket';
const kindLabel = skipKind ? '' : `<span class="se-tree-kind">${esc(node.kind)}</span>`;
const icon = depth === 0 && SECTION_ICONS[node.id]
? `<span class="se-section-icon">${SECTION_ICONS[node.id]}</span>`
: '';
const isBucket = node.kind === 'bucket';
const href = isBucket ? '' : `href="#/${esc(node.id)}"`;
const tag = isBucket ? 'div' : 'a';
const rowClass = `se-tree-row${isSelected ? ' se-selected' : ''}${isBucket ? ' se-tree-bucket' : ''}`;
const row = `
<${tag} class="${rowClass}" ${href}>
<span class="se-tree-toggle${hasChildren ? '' : ' se-leaf'}">${toggle}</span>
${icon}
<span class="se-tree-name">${esc(node.name)}</span>
${kindLabel}
</${tag}>`;
const children = hasChildren && isExpanded
? `<div class="se-tree-children">${node.children.map((c) => renderTreeNode(c, depth + 1, selected, autoExpand)).join('')}</div>`
: '';
return `<div class="se-tree-node">${row}${children}</div>`;
}