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