const typeRegistry = __TYPE_REGISTRY__;
const taxonomy = __TAXONOMY__;
function isInTypeTree(name) {
const t = typeRegistry[name];
if (!t) return false;
if (t.in_tree) return true;
if (t.extends) return isInTypeTree(t.extends);
return false;
}
const drawer = document.getElementById('drawer');
const overlay = document.getElementById('drawer-overlay');
const drawerTitle = drawer.querySelector('h2');
const drawerContent = drawer.querySelector('.content');
function openDrawer(title, htmlContent) {
drawerTitle.textContent = title;
drawerContent.innerHTML = htmlContent;
drawer.classList.add('open');
overlay.classList.add('open');
}
function closeDrawer() {
drawer.classList.remove('open');
overlay.classList.remove('open');
}
drawer.querySelector('.close').addEventListener('click', closeDrawer);
overlay.addEventListener('click', closeDrawer);
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
}
function renderTree(tree, highlight, depth = 0) {
if (typeof tree === 'string') {
return `
${escapeHtml(tree)}
`;
}
let out = '';
for (const k of Object.keys(tree)) {
const v = tree[k];
const isHL = (highlight && (k === highlight || k.endsWith('/'+highlight)));
const cls = isHL ? 'node highlight' : 'node';
out += `${escapeHtml(k)}${(typeof v === 'string') ? ' — ' + escapeHtml(v) : ''}
`;
if (typeof v === 'object') {
out += renderTree(v, highlight, depth + 1);
}
}
return out;
}
// type chip click
document.querySelectorAll('.chip[data-type]').forEach(c => {
c.addEventListener('click', () => {
const tp = c.getAttribute('data-type');
const t = typeRegistry[tp] || {};
let parts = [];
parts.push(`类型名: ${escapeHtml(tp)}
`);
if (t.extends) parts.push(`extends: ${escapeHtml(t.extends)}
`);
if (t.desc) parts.push(`描述: ${escapeHtml(t.desc)}
`);
parts.push(`字典树 (§A.3 类型词表)
`);
if (isInTypeTree(tp)) {
parts.push(`✓ ${escapeHtml(tp)} 是字典树叶子
`);
} else if (t.extends && isInTypeTree(t.extends)) {
parts.push(`${escapeHtml(tp)} 不在字典树叶子里, 但 extends ${escapeHtml(t.extends)} (字典树叶子). 这是 case-specific 类型扩展.
`);
} else {
parts.push(`${escapeHtml(tp)} 不在字典树叶子, 也无 extends 桥接.
`);
}
parts.push(`${renderTree(taxonomy['类型'].tree, tp)}
`);
openDrawer(`类型 · ${tp}`, parts.join(''));
});
});
// 作用 / 动作 / 特性 / 实质 / 形式 click
document.querySelectorAll('[data-prefix]').forEach(el => {
el.addEventListener('click', (e) => {
e.stopPropagation();
const prefix = el.getAttribute('data-prefix');
const value = el.getAttribute('data-value');
const tx = taxonomy[prefix];
if (!tx) {
openDrawer(`${prefix}`, `无字典树定义
`);
return;
}
let body = '';
body += `${escapeHtml(prefix)}: ${escapeHtml(tx.title || '')}
`;
if (tx.desc) body += `${escapeHtml(tx.desc)}
`;
if (tx.source === 'external') {
body += `${escapeHtml(prefix)} 维度词表过大, 维护在外部 JSON: ${escapeHtml(tx.file || '')}. 查询: spec/tools/taxonomy-lookup.py --dim ${escapeHtml(prefix)} --subtree <path>.
`;
body += `当前值
${escapeHtml(value)}
`;
} else if (tx.tree) {
body += `字典树 (当前值: ${escapeHtml(value)} 已高亮)
`;
body += `${renderTree(tx.tree, value)}
`;
}
openDrawer(`${prefix} · ${value}`, body);
});
});
// variable hover
document.querySelectorAll('.name[data-var]').forEach(n => {
n.addEventListener('mouseenter', () => {
const v = n.getAttribute('data-var');
document.querySelectorAll(`.name[data-var="${v}"]`).forEach(x => x.classList.add('var-highlight'));
});
n.addEventListener('mouseleave', () => {
document.querySelectorAll('.name.var-highlight').forEach(x => x.classList.remove('var-highlight'));
});
});
// block toggle (含 nested step-sub: 用 data-group 选)
document.querySelectorAll('tr.block-header').forEach(b => {
b.addEventListener('click', (e) => {
if (e.target.closest('.chip, [data-prefix], .name')) return;
const group = b.getAttribute('data-step');
const arrow = b.querySelector('.arrow');
const nested = document.querySelectorAll(`tr[data-group="${group}"]`);
const collapsed = arrow.textContent.trim() === '▶';
nested.forEach(n => n.style.display = collapsed ? '' : 'none');
arrow.textContent = collapsed ? '▼' : '▶';
});
});
// ───────── column visibility (legend toggle) ─────────
const COL_GROUPS = {
'demand': { headerSel: 'th.col-group-demand', cols: ['idx','intent','effect'] },
'input': { headerSel: 'th.col-group-input', cols: ['in-substance','in-form','in-type','in-name','in-value','in-anchor'] },
'impl': { headerSel: 'th.col-group-impl', cols: ['via','action','directive','config','decorator','memo','control','feature'] },
'output': { headerSel: 'th.col-group-output', cols: ['out-substance','out-form','out-type','out-name','out-value','out-anchor'] },
};
function setColVisible(col, visible) {
document.querySelectorAll(`th.col-${col}, td.${col}`).forEach(el => {
el.classList.toggle('col-hidden', !visible);
});
document.querySelectorAll(`.col-toggle[data-col="${col}"]`).forEach(b => {
b.classList.toggle('off', !visible);
});
recomputeGroupColspans();
if (typeof updateGroupHeadState === 'function') updateGroupHeadState();
}
function recomputeGroupColspans() {
for (const [, g] of Object.entries(COL_GROUPS)) {
const visibleCount = g.cols.filter(c => {
const th = document.querySelector(`th.col-${c}`);
return th && !th.classList.contains('col-hidden');
}).length;
const groupTh = document.querySelector(g.headerSel);
if (!groupTh) continue;
if (visibleCount === 0) {
groupTh.classList.add('col-hidden');
} else {
groupTh.classList.remove('col-hidden');
groupTh.setAttribute('colspan', String(visibleCount));
}
}
}
document.querySelectorAll('.col-toggle').forEach(btn => {
btn.addEventListener('click', () => {
const col = btn.getAttribute('data-col');
const isOff = btn.classList.contains('off');
setColVisible(col, isOff);
});
});
document.querySelectorAll('.legend .group .gh').forEach(gh => {
gh.addEventListener('click', () => {
const grpEl = gh.closest('.group');
const toggles = Array.from(grpEl.querySelectorAll('.col-toggle'));
const anyVisible = toggles.some(t => !t.classList.contains('off'));
toggles.forEach(t => setColVisible(t.getAttribute('data-col'), !anyVisible));
updateGroupHeadState();
});
});
function updateGroupHeadState() {
document.querySelectorAll('.legend .group').forEach(grpEl => {
const toggles = Array.from(grpEl.querySelectorAll('.col-toggle'));
const allOff = toggles.every(t => t.classList.contains('off'));
grpEl.querySelector('.gh').classList.toggle('all-off', allOff);
});
}
recomputeGroupColspans();
updateGroupHeadState();
// 推断补全 toggle (legend 上的"高亮推断")
const infBtn = document.getElementById('inferred-toggle');
if (infBtn) {
infBtn.addEventListener('click', () => {
document.body.classList.toggle('show-inferred');
infBtn.classList.toggle('on');
});
}
// ───────── 原子能力 atom badge toggle ─────────
// 点击 step 行 idx 列的 ⚛N 徽章, 展开/收起该 step 下的 atom 子行 (默认全收起)
document.querySelectorAll('.atom-badge').forEach(b => {
b.addEventListener('click', (e) => {
e.stopPropagation();
const step = b.getAttribute('data-step');
const atoms = document.querySelectorAll(`tr.atom-row[data-atom-of="${step}"]`);
const open = b.classList.toggle('open');
atoms.forEach(a => a.classList.toggle('show', open));
const arrow = b.querySelector('.atom-arrow');
if (arrow) arrow.textContent = open ? '▾' : '▸';
});
});
// ───────── 跨页跳转: 处理 URL hash 指向 atom ─────────
// 当 URL 是 case-N.html#sX-aY 时, 自动展开该 atom 的全部子行 + 打开父 step 徽章
// (单独的 :target CSS 只对主行生效, sub-rows 仍隐藏, 需 JS 补)
function expandTargetAtom() {
const hash = decodeURIComponent(location.hash.slice(1));
if (!hash) return;
const target = document.getElementById(hash);
if (!target || !target.classList.contains('atom-row')) return;
const step = target.getAttribute('data-step');
const parent = target.getAttribute('data-atom-of');
document.querySelectorAll(`tr.atom-row[data-step="${step}"][data-atom-of="${parent}"]`)
.forEach(r => r.classList.add('show'));
const badge = document.querySelector(`.atom-badge[data-step="${parent}"]`);
if (badge) {
badge.classList.add('open');
const arrow = badge.querySelector('.atom-arrow');
if (arrow) arrow.textContent = '▾';
}
setTimeout(() => target.scrollIntoView({behavior: 'smooth', block: 'center'}), 80);
}
expandTargetAtom();
window.addEventListener('hashchange', expandTargetAtom);