script.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. const typeRegistry = __TYPE_REGISTRY__;
  2. const taxonomy = __TAXONOMY__;
  3. function isInTypeTree(name) {
  4. const t = typeRegistry[name];
  5. if (!t) return false;
  6. if (t.in_tree) return true;
  7. if (t.extends) return isInTypeTree(t.extends);
  8. return false;
  9. }
  10. const drawer = document.getElementById('drawer');
  11. const overlay = document.getElementById('drawer-overlay');
  12. const drawerTitle = drawer.querySelector('h2');
  13. const drawerContent = drawer.querySelector('.content');
  14. function openDrawer(title, htmlContent) {
  15. drawerTitle.textContent = title;
  16. drawerContent.innerHTML = htmlContent;
  17. drawer.classList.add('open');
  18. overlay.classList.add('open');
  19. }
  20. function closeDrawer() {
  21. drawer.classList.remove('open');
  22. overlay.classList.remove('open');
  23. }
  24. drawer.querySelector('.close').addEventListener('click', closeDrawer);
  25. overlay.addEventListener('click', closeDrawer);
  26. function escapeHtml(s) {
  27. return String(s).replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));
  28. }
  29. function renderTree(tree, highlight, depth = 0) {
  30. if (typeof tree === 'string') {
  31. return `<div class="node" style="margin-left:${depth*16}px; color:#6b7280;">${escapeHtml(tree)}</div>`;
  32. }
  33. let out = '';
  34. for (const k of Object.keys(tree)) {
  35. const v = tree[k];
  36. const isHL = (highlight && (k === highlight || k.endsWith('/'+highlight)));
  37. const cls = isHL ? 'node highlight' : 'node';
  38. out += `<div class="${cls}" style="margin-left:${depth*16}px;">${escapeHtml(k)}${(typeof v === 'string') ? ' — ' + escapeHtml(v) : ''}</div>`;
  39. if (typeof v === 'object') {
  40. out += renderTree(v, highlight, depth + 1);
  41. }
  42. }
  43. return out;
  44. }
  45. // type chip click
  46. document.querySelectorAll('.chip[data-type]').forEach(c => {
  47. c.addEventListener('click', () => {
  48. const tp = c.getAttribute('data-type');
  49. const t = typeRegistry[tp] || {};
  50. let parts = [];
  51. parts.push(`<div class="row"><b>类型名</b>: ${escapeHtml(tp)}</div>`);
  52. if (t.extends) parts.push(`<div class="row"><b>extends</b>: ${escapeHtml(t.extends)}</div>`);
  53. if (t.desc) parts.push(`<div class="row"><b>描述</b>: ${escapeHtml(t.desc)}</div>`);
  54. parts.push(`<h3>字典树 (§A.3 类型词表)</h3>`);
  55. if (t && t.in_tree) {
  56. parts.push(`<div class="row" style="color:#047857;">✓ <b>${escapeHtml(tp)}</b> 是字典树叶子</div>`);
  57. } else if (t && t.extends && isInTypeTree(t.extends)) {
  58. parts.push(`<div class="row" style="color:#0369a1;">✓ <b>${escapeHtml(tp)}</b> 是自定义拓展节点 (继承自: <b>${escapeHtml(t.extends)}</b>)</div>`);
  59. } else {
  60. parts.push(`<div class="warning">${escapeHtml(tp)} 不在字典树叶子, 也无合法的 extends 桥接.</div>`);
  61. }
  62. parts.push(`<div class="tree">${renderTree(taxonomy['类型'].tree, tp)}</div>`);
  63. openDrawer(`类型 · ${tp}`, parts.join(''));
  64. });
  65. });
  66. // 作用 / 动作 / 特性 / 实质 / 形式 click
  67. document.querySelectorAll('[data-prefix]').forEach(el => {
  68. el.addEventListener('click', (e) => {
  69. e.stopPropagation();
  70. const prefix = el.getAttribute('data-prefix');
  71. const value = el.getAttribute('data-value');
  72. const tx = taxonomy[prefix];
  73. if (!tx) {
  74. openDrawer(`${prefix}`, `<div class="warning">无字典树定义</div>`);
  75. return;
  76. }
  77. let body = '';
  78. body += `<div class="row"><b>${escapeHtml(prefix)}</b>: ${escapeHtml(tx.title || '')}</div>`;
  79. if (tx.desc) body += `<div class="row" style="color:#6b7280;">${escapeHtml(tx.desc)}</div>`;
  80. if (tx.source === 'external') {
  81. body += `<div class="warning"><b>${escapeHtml(prefix)}</b> 维度词表过大, 维护在外部 JSON: <code>${escapeHtml(tx.file || '')}</code>. 查询: <code>spec/tools/taxonomy-lookup.py --dim ${escapeHtml(prefix)} --subtree &lt;path&gt;</code>.</div>`;
  82. body += `<h3>当前值</h3><div class="row" style="font-family:ui-monospace,monospace;">${escapeHtml(value)}</div>`;
  83. } else if (tx.tree) {
  84. body += `<h3>字典树 (当前值: <span class="highlight">${escapeHtml(value)}</span> 已高亮)</h3>`;
  85. body += `<div class="tree">${renderTree(tx.tree, value)}</div>`;
  86. }
  87. openDrawer(`${prefix} · ${value}`, body);
  88. });
  89. });
  90. // variable hover
  91. document.querySelectorAll('.name[data-var]').forEach(n => {
  92. n.addEventListener('mouseenter', () => {
  93. const v = n.getAttribute('data-var');
  94. document.querySelectorAll(`.name[data-var="${v}"]`).forEach(x => x.classList.add('var-highlight'));
  95. });
  96. n.addEventListener('mouseleave', () => {
  97. document.querySelectorAll('.name.var-highlight').forEach(x => x.classList.remove('var-highlight'));
  98. });
  99. });
  100. // block toggle (含 nested step-sub: 用 data-group 选)
  101. document.querySelectorAll('tr.block-header').forEach(b => {
  102. b.addEventListener('click', (e) => {
  103. if (e.target.closest('.chip, [data-prefix], .name')) return;
  104. const group = b.getAttribute('data-step');
  105. const arrow = b.querySelector('.arrow');
  106. const nested = document.querySelectorAll(`tr[data-group="${group}"]`);
  107. const collapsed = arrow.textContent.trim() === '▶';
  108. nested.forEach(n => n.style.display = collapsed ? '' : 'none');
  109. arrow.textContent = collapsed ? '▼' : '▶';
  110. });
  111. });
  112. // ───────── column visibility (legend toggle) ─────────
  113. const COL_GROUPS = {
  114. 'demand': { headerSel: 'th.col-group-demand', cols: ['idx','intent','effect'] },
  115. 'input': { headerSel: 'th.col-group-input', cols: ['in-substance','in-form','in-type','in-name','in-value','in-anchor'] },
  116. 'impl': { headerSel: 'th.col-group-impl', cols: ['via','action','directive','config','decorator','memo','control','feature'] },
  117. 'output': { headerSel: 'th.col-group-output', cols: ['out-substance','out-form','out-type','out-name','out-value','out-anchor'] },
  118. };
  119. function setColVisible(col, visible) {
  120. document.querySelectorAll(`th.col-${col}, td.${col}`).forEach(el => {
  121. el.classList.toggle('col-hidden', !visible);
  122. });
  123. document.querySelectorAll(`.col-toggle[data-col="${col}"]`).forEach(b => {
  124. b.classList.toggle('off', !visible);
  125. });
  126. recomputeGroupColspans();
  127. if (typeof updateGroupHeadState === 'function') updateGroupHeadState();
  128. }
  129. function recomputeGroupColspans() {
  130. for (const [, g] of Object.entries(COL_GROUPS)) {
  131. const visibleCount = g.cols.filter(c => {
  132. const th = document.querySelector(`th.col-${c}`);
  133. return th && !th.classList.contains('col-hidden');
  134. }).length;
  135. const groupTh = document.querySelector(g.headerSel);
  136. if (!groupTh) continue;
  137. if (visibleCount === 0) {
  138. groupTh.classList.add('col-hidden');
  139. } else {
  140. groupTh.classList.remove('col-hidden');
  141. groupTh.setAttribute('colspan', String(visibleCount));
  142. }
  143. }
  144. }
  145. document.querySelectorAll('.col-toggle').forEach(btn => {
  146. btn.addEventListener('click', () => {
  147. const col = btn.getAttribute('data-col');
  148. const isOff = btn.classList.contains('off');
  149. setColVisible(col, isOff);
  150. });
  151. });
  152. document.querySelectorAll('.legend .group .gh').forEach(gh => {
  153. gh.addEventListener('click', () => {
  154. const grpEl = gh.closest('.group');
  155. const toggles = Array.from(grpEl.querySelectorAll('.col-toggle'));
  156. const anyVisible = toggles.some(t => !t.classList.contains('off'));
  157. toggles.forEach(t => setColVisible(t.getAttribute('data-col'), !anyVisible));
  158. updateGroupHeadState();
  159. });
  160. });
  161. function updateGroupHeadState() {
  162. document.querySelectorAll('.legend .group').forEach(grpEl => {
  163. const toggles = Array.from(grpEl.querySelectorAll('.col-toggle'));
  164. const allOff = toggles.every(t => t.classList.contains('off'));
  165. grpEl.querySelector('.gh').classList.toggle('all-off', allOff);
  166. });
  167. }
  168. recomputeGroupColspans();
  169. updateGroupHeadState();
  170. // 推断补全 toggle (legend 上的"高亮推断")
  171. const infBtn = document.getElementById('inferred-toggle');
  172. if (infBtn) {
  173. infBtn.addEventListener('click', () => {
  174. document.body.classList.toggle('show-inferred');
  175. infBtn.classList.toggle('on');
  176. });
  177. }
  178. // ───────── 原子能力 atom badge toggle ─────────
  179. // 点击 step 行 idx 列的 ⚛N 徽章, 展开/收起该 step 下的 atom 子行 (默认全收起)
  180. document.querySelectorAll('.atom-badge').forEach(b => {
  181. b.addEventListener('click', (e) => {
  182. e.stopPropagation();
  183. const step = b.getAttribute('data-step');
  184. const atoms = document.querySelectorAll(`tr.atom-row[data-atom-of="${step}"]`);
  185. const open = b.classList.toggle('open');
  186. atoms.forEach(a => a.classList.toggle('show', open));
  187. const arrow = b.querySelector('.atom-arrow');
  188. if (arrow) arrow.textContent = open ? '▾' : '▸';
  189. });
  190. });
  191. // ───────── 跨页跳转: 处理 URL hash 指向 atom ─────────
  192. // 当 URL 是 case-N.html#sX-aY 时, 自动展开该 atom 的全部子行 + 打开父 step 徽章
  193. // (单独的 :target CSS 只对主行生效, sub-rows 仍隐藏, 需 JS 补)
  194. function expandTargetAtom() {
  195. const hash = decodeURIComponent(location.hash.slice(1));
  196. if (!hash) return;
  197. const target = document.getElementById(hash);
  198. if (!target || !target.classList.contains('atom-row')) return;
  199. const step = target.getAttribute('data-step');
  200. const parent = target.getAttribute('data-atom-of');
  201. document.querySelectorAll(`tr.atom-row[data-step="${step}"][data-atom-of="${parent}"]`)
  202. .forEach(r => r.classList.add('show'));
  203. const badge = document.querySelector(`.atom-badge[data-step="${parent}"]`);
  204. if (badge) {
  205. badge.classList.add('open');
  206. const arrow = badge.querySelector('.atom-arrow');
  207. if (arrow) arrow.textContent = '▾';
  208. }
  209. setTimeout(() => target.scrollIntoView({behavior: 'smooth', block: 'center'}), 80);
  210. }
  211. expandTargetAtom();
  212. window.addEventListener('hashchange', expandTargetAtom);