| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- 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 `<div class="node" style="margin-left:${depth*16}px; color:#6b7280;">${escapeHtml(tree)}</div>`;
- }
- 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 += `<div class="${cls}" style="margin-left:${depth*16}px;">${escapeHtml(k)}${(typeof v === 'string') ? ' — ' + escapeHtml(v) : ''}</div>`;
- 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(`<div class="row"><b>类型名</b>: ${escapeHtml(tp)}</div>`);
- if (t.extends) parts.push(`<div class="row"><b>extends</b>: ${escapeHtml(t.extends)}</div>`);
- if (t.desc) parts.push(`<div class="row"><b>描述</b>: ${escapeHtml(t.desc)}</div>`);
- parts.push(`<h3>字典树 (§A.3 类型词表)</h3>`);
- if (isInTypeTree(tp)) {
- parts.push(`<div class="row" style="color:#047857;">✓ <b>${escapeHtml(tp)}</b> 是字典树叶子</div>`);
- } else if (t.extends && isInTypeTree(t.extends)) {
- parts.push(`<div class="warning">${escapeHtml(tp)} 不在字典树叶子里, 但 extends <b>${escapeHtml(t.extends)}</b> (字典树叶子). 这是 case-specific 类型扩展.</div>`);
- } else {
- parts.push(`<div class="warning">${escapeHtml(tp)} 不在字典树叶子, 也无 extends 桥接.</div>`);
- }
- parts.push(`<div class="tree">${renderTree(taxonomy['类型'].tree, tp)}</div>`);
- 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}`, `<div class="warning">无字典树定义</div>`);
- return;
- }
- let body = '';
- body += `<div class="row"><b>${escapeHtml(prefix)}</b>: ${escapeHtml(tx.title || '')}</div>`;
- if (tx.desc) body += `<div class="row" style="color:#6b7280;">${escapeHtml(tx.desc)}</div>`;
- if (tx.source === 'external') {
- body += `<div class="warning"><b>${escapeHtml(prefix)}</b> 维度词表过大, 维护在外部 JSON: <code>${escapeHtml(tx.file || '')}</code>. 查询: <code>spec/tools/taxonomy-lookup.py --dim ${escapeHtml(prefix)} --subtree <path></code>.</div>`;
- body += `<h3>当前值</h3><div class="row" style="font-family:ui-monospace,monospace;">${escapeHtml(value)}</div>`;
- } else if (tx.tree) {
- body += `<h3>字典树 (当前值: <span class="highlight">${escapeHtml(value)}</span> 已高亮)</h3>`;
- body += `<div class="tree">${renderTree(tx.tree, value)}</div>`;
- }
- 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);
|