You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
292 lines
9.6 KiB
292 lines
9.6 KiB
|
1 week ago
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
解析 C 文件中的 TKeyForest 初始化表,生成全局共享模式的 HTML 交互树状图。
|
||
|
|
|
||
|
|
用法:
|
||
|
|
python gen_shared_html_tree.py <c_file> [table_name]
|
||
|
|
"""
|
||
|
|
|
||
|
|
import re
|
||
|
|
import sys
|
||
|
|
import os
|
||
|
|
from collections import defaultdict
|
||
|
|
|
||
|
|
def parse_table(c_file, table_name):
|
||
|
|
"""从 .c 文件中提取并解析 TKeyForest 初始化表"""
|
||
|
|
with open(c_file, "r", encoding="utf-8") as f:
|
||
|
|
content = f.read()
|
||
|
|
|
||
|
|
# 1. 定位初始化块
|
||
|
|
pat = re.compile(
|
||
|
|
r'(?:const\s+)?TKeyForest\s+' + re.escape(table_name) +
|
||
|
|
r'\s*=\s*\{(.*)\}\s*;',
|
||
|
|
re.DOTALL
|
||
|
|
)
|
||
|
|
m = pat.search(content)
|
||
|
|
if not m:
|
||
|
|
print(f"错误:未找到 {table_name} 的初始化块", file=sys.stderr)
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
body = m.group(1)
|
||
|
|
key_blocks = []
|
||
|
|
pos = 0
|
||
|
|
|
||
|
|
# 2. 拆分各个 Key
|
||
|
|
while pos < len(body):
|
||
|
|
km = re.search(r'\.(\w+)\s*=\s*\{', body[pos:])
|
||
|
|
if not km:
|
||
|
|
break
|
||
|
|
field_name = km.group(1)
|
||
|
|
start = pos + km.end()
|
||
|
|
|
||
|
|
depth = 1
|
||
|
|
i = start
|
||
|
|
while i < len(body) and depth > 0:
|
||
|
|
if body[i] == '{': depth += 1
|
||
|
|
elif body[i] == '}': depth -= 1
|
||
|
|
i += 1
|
||
|
|
|
||
|
|
block_content = body[start:i - 1]
|
||
|
|
key_blocks.append((field_name, block_content))
|
||
|
|
pos = i
|
||
|
|
|
||
|
|
# 3. 解析三元组 {Mode, SubMode, Func}
|
||
|
|
# 使用集合去重,确保相同的 Mode->Func 关系只被记录一次
|
||
|
|
edges = set()
|
||
|
|
all_keys = set()
|
||
|
|
|
||
|
|
for field_name, block in key_blocks:
|
||
|
|
all_keys.add(field_name)
|
||
|
|
block_clean = re.sub(r'//.*', '', block) # 去除注释
|
||
|
|
for em in re.finditer(r'\{([^}]+)\}', block_clean):
|
||
|
|
parts = [p.strip().strip(',') for p in em.group(1).split(',')]
|
||
|
|
if len(parts) >= 3:
|
||
|
|
mode, sub_mode, func = parts[0], parts[1], parts[2]
|
||
|
|
if func != "NULL":
|
||
|
|
# 记录边关系:Key -> Mode -> Func
|
||
|
|
edges.add((field_name, mode, func))
|
||
|
|
|
||
|
|
return list(all_keys), edges
|
||
|
|
|
||
|
|
def build_html(keys, edges):
|
||
|
|
"""构建包含 CSS 和 JS 的完整 HTML 字符串"""
|
||
|
|
|
||
|
|
# 预处理数据,计算每个节点的入度和出度
|
||
|
|
node_stats = defaultdict(lambda: {'in': 0, 'out': 0})
|
||
|
|
|
||
|
|
# 用于生成唯一 ID,避免特殊字符
|
||
|
|
def get_id(name):
|
||
|
|
return name.replace('-', '_').replace('(', '').replace(')', '')
|
||
|
|
|
||
|
|
for key, mode, func in edges:
|
||
|
|
k_id = get_id(key)
|
||
|
|
m_id = get_id(mode)
|
||
|
|
f_id = get_id(func)
|
||
|
|
|
||
|
|
node_stats[k_id]['out'] += 1
|
||
|
|
node_stats[m_id]['in'] += 1
|
||
|
|
node_stats[m_id]['out'] += 1
|
||
|
|
node_stats[f_id]['in'] += 1
|
||
|
|
|
||
|
|
# 构建 HTML 结构
|
||
|
|
html_parts = []
|
||
|
|
html_parts.append("""
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="zh-CN">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<title>KeyForest 全局共享树状图</title>
|
||
|
|
<style>
|
||
|
|
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f4f6f9; padding: 20px; }
|
||
|
|
h2 { text-align: center; color: #333; }
|
||
|
|
.container { display: flex; flex-direction: column; gap: 10px; align-items: center; }
|
||
|
|
|
||
|
|
/* 节点样式 */
|
||
|
|
.node {
|
||
|
|
padding: 8px 15px; margin: 5px; border-radius: 6px; cursor: pointer;
|
||
|
|
transition: all 0.3s; border: 1px solid #ccc; display: inline-block;
|
||
|
|
font-size: 14px; background: white; position: relative;
|
||
|
|
}
|
||
|
|
.node:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
|
||
|
|
|
||
|
|
/* 层级样式 */
|
||
|
|
.layer-keys { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-bottom: 20px; }
|
||
|
|
.layer-modes { display: flex; flex-wrap: wrap; justify-content: center; gap: 15px; margin-bottom: 20px; }
|
||
|
|
.layer-funcs { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }
|
||
|
|
|
||
|
|
/* 颜色区分 */
|
||
|
|
.type-key { background: #e3f2fd; border-color: #90caf9; color: #0d47a1; font-weight: bold; }
|
||
|
|
.type-mode { background: #fff3e0; border-color: #ffcc80; color: #e65100; font-weight: bold; }
|
||
|
|
.type-func { background: #e8f5e9; border-color: #a5d6a7; color: #1b5e20; font-family: monospace; }
|
||
|
|
|
||
|
|
/* 高亮与变暗 */
|
||
|
|
.dimmed { opacity: 0.15; filter: grayscale(100%); }
|
||
|
|
.highlight {
|
||
|
|
background: #d4edda !important; border-color: #28a745 !important;
|
||
|
|
color: #155724; transform: scale(1.1); z-index: 10; box-shadow: 0 0 15px rgba(40, 167, 69, 0.5);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* 连线模拟 (使用伪元素) */
|
||
|
|
.connector { height: 20px; width: 2px; background: #ccc; margin: 0 auto; }
|
||
|
|
.connector.highlight { background: #28a745; }
|
||
|
|
|
||
|
|
/* 提示框 */
|
||
|
|
#tooltip {
|
||
|
|
position: fixed; display: none; background: rgba(0,0,0,0.85); color: white;
|
||
|
|
padding: 10px; border-radius: 5px; font-size: 13px; pointer-events: none; z-index: 1000;
|
||
|
|
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<h2>TKeyForest 全局共享交互图 (单击节点高亮路径)</h2>
|
||
|
|
<div class="container">
|
||
|
|
<!-- 第一层:Keys -->
|
||
|
|
<div class="layer-keys" id="layer-keys">
|
||
|
|
""")
|
||
|
|
|
||
|
|
# 渲染 Keys
|
||
|
|
for key in sorted(keys):
|
||
|
|
k_id = get_id(key)
|
||
|
|
stats = node_stats[k_id]
|
||
|
|
html_parts.append(f'<div class="node type-key" id="{k_id}" data-in="{stats["in"]}" data-out="{stats["out"]}" onclick="handleClick(this)">{key}</div>')
|
||
|
|
|
||
|
|
html_parts.append("""
|
||
|
|
</div>
|
||
|
|
<div class="connector" id="conn-1"></div>
|
||
|
|
|
||
|
|
<!-- 第二层:Modes -->
|
||
|
|
<div class="layer-modes" id="layer-modes">
|
||
|
|
""")
|
||
|
|
|
||
|
|
# 收集所有唯一的 Modes
|
||
|
|
all_modes = sorted(set(mode for _, mode, _ in edges))
|
||
|
|
for mode in all_modes:
|
||
|
|
m_id = get_id(mode)
|
||
|
|
stats = node_stats[m_id]
|
||
|
|
html_parts.append(f'<div class="node type-mode" id="{m_id}" data-in="{stats["in"]}" data-out="{stats["out"]}" onclick="handleClick(this)">{mode}</div>')
|
||
|
|
|
||
|
|
html_parts.append("""
|
||
|
|
</div>
|
||
|
|
<div class="connector" id="conn-2"></div>
|
||
|
|
|
||
|
|
<!-- 第三层:Funcs -->
|
||
|
|
<div class="layer-funcs" id="layer-funcs">
|
||
|
|
""")
|
||
|
|
|
||
|
|
# 收集所有唯一的 Funcs
|
||
|
|
all_funcs = sorted(set(func for _, _, func in edges))
|
||
|
|
for func in all_funcs:
|
||
|
|
f_id = get_id(func)
|
||
|
|
stats = node_stats[f_id]
|
||
|
|
html_parts.append(f'<div class="node type-func" id="{f_id}" data-in="{stats["in"]}" data-out="{stats["out"]}" onclick="handleClick(this)">{func}()</div>')
|
||
|
|
|
||
|
|
html_parts.append("""
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div id="tooltip"></div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
// 预计算所有边关系,用于高亮逻辑
|
||
|
|
const edges = [];
|
||
|
|
""")
|
||
|
|
|
||
|
|
# 将 Python 解析的边数据注入到 JS 中
|
||
|
|
for key, mode, func in edges:
|
||
|
|
html_parts.append(f'edges.push(["{get_id(key)}", "{get_id(mode)}", "{get_id(func)}"]);')
|
||
|
|
|
||
|
|
html_parts.append("""
|
||
|
|
let activeNode = null;
|
||
|
|
|
||
|
|
function handleClick(node) {
|
||
|
|
if (activeNode === node) {
|
||
|
|
resetHighlight();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
resetHighlight();
|
||
|
|
activeNode = node;
|
||
|
|
const nodeId = node.id;
|
||
|
|
|
||
|
|
// 找出所有经过该节点的路径
|
||
|
|
const relatedNodes = new Set();
|
||
|
|
const relatedConns = new Set();
|
||
|
|
|
||
|
|
// 1. 如果点击的是 Key,高亮 Key -> Mode -> Func
|
||
|
|
// 2. 如果点击的是 Mode,高亮 Key -> Mode -> Func
|
||
|
|
// 3. 如果点击的是 Func,高亮 Key -> Mode -> Func
|
||
|
|
|
||
|
|
edges.forEach(([k, m, f]) => {
|
||
|
|
if (k === nodeId || m === nodeId || f === nodeId) {
|
||
|
|
relatedNodes.add(k);
|
||
|
|
relatedNodes.add(m);
|
||
|
|
relatedNodes.add(f);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 应用高亮
|
||
|
|
relatedNodes.forEach(id => {
|
||
|
|
document.getElementById(id).classList.add('highlight');
|
||
|
|
});
|
||
|
|
|
||
|
|
// 变暗无关节点
|
||
|
|
document.querySelectorAll('.node').forEach(n => {
|
||
|
|
if (!relatedNodes.has(n.id)) {
|
||
|
|
n.classList.add('dimmed');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 变暗连线
|
||
|
|
document.querySelectorAll('.connector').forEach(c => c.classList.add('dimmed'));
|
||
|
|
}
|
||
|
|
|
||
|
|
function resetHighlight() {
|
||
|
|
document.querySelectorAll('.highlight').forEach(el => el.classList.remove('highlight'));
|
||
|
|
document.querySelectorAll('.dimmed').forEach(el => el.classList.remove('dimmed'));
|
||
|
|
activeNode = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 鼠标悬停显示统计信息
|
||
|
|
const tooltip = document.getElementById('tooltip');
|
||
|
|
document.querySelectorAll('.node').forEach(node => {
|
||
|
|
node.addEventListener('mousemove', (e) => {
|
||
|
|
const inDeg = node.getAttribute('data-in');
|
||
|
|
const outDeg = node.getAttribute('data-out');
|
||
|
|
tooltip.style.display = 'block';
|
||
|
|
tooltip.style.left = (e.pageX + 15) + 'px';
|
||
|
|
tooltip.style.top = (e.pageY + 15) + 'px';
|
||
|
|
tooltip.innerHTML = `<b>${node.innerText}</b><br>入度(指向此节点): ${inDeg}<br>出度(从此指出): ${outDeg}`;
|
||
|
|
});
|
||
|
|
node.addEventListener('mouseleave', () => {
|
||
|
|
tooltip.style.display = 'none';
|
||
|
|
});
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
""")
|
||
|
|
|
||
|
|
return "".join(html_parts)
|
||
|
|
|
||
|
|
def main():
|
||
|
|
if len(sys.argv) < 2:
|
||
|
|
print("用法: python gen_shared_html_tree.py <c_file> [table_name]", file=sys.stderr)
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
c_file = sys.argv[1]
|
||
|
|
table_name = sys.argv[2] if len(sys.argv) > 2 else "g_ActionTable"
|
||
|
|
|
||
|
|
if not os.path.isfile(c_file):
|
||
|
|
print(f"错误:文件不存在: {c_file}", file=sys.stderr)
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
keys, edges = parse_table(c_file, table_name)
|
||
|
|
html_content = build_html(keys, edges)
|
||
|
|
|
||
|
|
out_file = os.path.splitext(c_file)[0] + "_shared_tree.html"
|
||
|
|
with open(out_file, "w", encoding="utf-8") as f:
|
||
|
|
f.write(html_content)
|
||
|
|
|
||
|
|
print(f"成功生成全局共享 HTML 树状图: {out_file}")
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|