function nodeCrumbsHtml(node, level) {
if (isExternalNode(node, level)) return `<span class="nm-title">${escHtml(node.name)}</span>`;
const tier = window.viewTier(level);
const rootLabel = tier === 'file' ? 'root' : 'all';
const parts = [
`<span class="crumb-tier">${window.tierAnchorHtml(level, tier)}</span>`,
`<span class="drill-sep">›</span>`,
`<button class="drill-crumb" data-mc-root="1" type="button" title="Show the whole overview">${rootLabel}</button>`,
];
const tgt = focusFolderTarget(level, node); if (tgt.key && tgt.key !== '_root') {
const segs = String(tgt.key).split('/');
for (let i = 0; i < segs.length; i++) {
const key = segs.slice(0, i + 1).join('/');
const dg = chipDig(level, i, tier);
parts.push('<span class="drill-sep">›</span>');
parts.push(`<button class="drill-crumb" data-mc-key="${escAttr(key)}" data-mc-dig="${dg}" type="button">${escHtml(segs[i])}</button>`);
}
}
parts.push('<span class="drill-sep">›</span>');
parts.push(`<span class="drill-crumb-cur nm-crumb-file">${escHtml(node.name)}</span>`);
return `<span class="nm-crumbs">${parts.join(' ')}</span>`;
}
function buildModalContent(node, level) {
const cycles = window.CYCLES?.[level];
const cs = cycles?.nodeCycleStatus?.get(node.id);
const mnExt = isExternalNode(node, level);
const path = mnExt ? (node.path || node.id || '')
: (node.path || node.id || '').replace(/^\{[^}]+\}\//, '');
const absFull = absPath(mnExt ? (node.path || node.id) : node.id);
const vis = typeof node.visibility === 'string' ? node.visibility : null;
const sections = [];
let cur = { label: null, rows: [] };
const tipAttr = escAttr;
const row = (key, v, opts) => {
if (v == null || v === '') return;
const label = attrLabel(level, key) || (key.charAt(0).toUpperCase() + key.slice(1));
const title = attrName(level, key) || label;
const desc = attrDesc(level, key);
const formula = attrFormula(level, key);
const calc = opts?.calc || '';
const attr = desc
? ` data-tip="${tipAttr(desc)}" data-tip-title="${tipAttr(title)}"` +
(formula ? ` data-tip-formula="${tipAttr(formula)}"` : '') +
(calc ? ` data-tip-calc="${tipAttr(calc)}"` : '')
: '';
cur.rows.push(`<tr${attr}><td class="nm-key">${label}</td><td class="nm-val">${v}</td></tr>`);
};
const rawRow = (label, valHtml, tipTitle, tipDesc) => {
const attr = tipDesc
? ` data-tip="${tipAttr(tipDesc)}" data-tip-title="${tipAttr(tipTitle || label)}"`
: '';
cur.rows.push(`<tr${attr}><td class="nm-key">${label}</td><td class="nm-val">${valHtml}</td></tr>`);
};
const sect = label => { sections.push(cur); cur = { label, rows: [] }; };
const esc = escAttr;
if (path) {
const si = path.lastIndexOf('/');
const dir = si >= 0 ? esc(path.slice(0, si + 1)) : '';
const file = esc(si >= 0 ? path.slice(si + 1) : path);
rawRow('Path',
`${dir}<strong>${file}</strong>`,
attrName(level, 'path') || 'Path',
absFull || attrDesc(level, 'path') || 'Location of this node.'
);
if (!mnExt) {
const url = nodeSourceUrl(node, level);
if (url) {
const host = url.replace(/^https?:\/\//i, '').split('/')[0];
cur.rows.push(
`<tr><td class="nm-key">Source</td><td class="nm-val">` +
`<a class="nm-src" href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(host)} ↗</a>` +
`</td></tr>`
);
}
}
}
if (mnExt) row('id', node.id);
row('kind', node.kind || null);
row('version', node.version ?? null);
if (mnExt) row('external', 'true');
if (vis && vis !== 'public') row('visibility', vis);
if (node.items != null) row('items', fmtFull(node.items));
if (node.cycle != null) row('cycle', node.cycle);
if (cs && cs !== 'none') rawRow('Cycle status', cs, 'Cycle status', 'Whether this cycle exists on the baseline side, current side, or both.');
if (!document.body.classList.contains('mode-review')) row('status', node.status);
const numKeys = numericAttrKeys(level);
const groups = attributeGroups(level);
const grouped = {}; const ungrouped = []; for (const k of numKeys) {
const v = nodeAttr(node, k);
if (v == null) continue;
const g = attrGroup(level, k);
if (g) {
if (!grouped[g]) grouped[g] = [];
grouped[g].push(k);
} else {
ungrouped.push(k);
}
}
if (ungrouped.length > 0) {
sect(null);
for (const k of ungrouped) {
const v = nodeAttr(node, k);
row(k, fmtFull(v), { calc: calcDisplay(level, k, node) });
}
}
const groupOrder = Object.keys(groups);
const allGroupIds = [
...groupOrder.filter(g => grouped[g]),
...Object.keys(grouped).filter(g => !groupOrder.includes(g)),
];
for (const gId of allGroupIds) {
const keys = grouped[gId];
if (!keys || keys.length === 0) continue;
const gLabel = groups[gId]?.label || gId;
sect(gLabel);
for (const k of keys) {
const v = nodeAttr(node, k);
row(k, fmtFull(v), { calc: calcDisplay(level, k, node) });
}
}
sections.push(cur);
const renderSect = s =>
`${s.label ? `<div class="nm-sect-label">${s.label}</div>` : ''}` +
`<table class="nm-table">${s.rows.join('')}</table>`;
const body = sections.filter(s => s.rows.length > 0).map(renderSect).join('');
return {
hdr: nodeHeaderHtml(node, level),
body,
diagram: buildDiagramSVG(node, level),
};
}
function nodeHeaderHtml(node, level) {
const sideSuffix = (typeof viewModeSuffix === 'function') ? viewModeSuffix().trim() : '';
return nodeCrumbsHtml(node, level) + `<span class="nm-badge">${escHtml(node.kind)}</span>` +
(sideSuffix ? `<span class="nm-side">${escHtml(sideSuffix)}</span>` : '');
}
window.nodeHeaderHtml = nodeHeaderHtml;