// js/chatbot/utils/drawio-lite-parser.js
/**
* DrawioLite DSL Parser
* 将极简文本语法转换为 Draw.io mxGraph XML
*
* 支持功能:
* - 基本节点和连接
* - 分组/容器 (swimlane)
* - 图例 (legend)
* - 并列子图 (subgraph) - 复杂多图支持
* - 多页图表 (page) - 复杂多图支持
*
* @version 1.1.0 - 支持复杂多图
* @date 2025-01-16
*/
/**
* 颜色预设(学术标准)
*/
const COLOR_PRESETS = {
gray: { fill: '#F7F9FC', stroke: '#2C3E50' },
blue: { fill: '#dae8fc', stroke: '#3498DB' },
lightblue: { fill: '#dae8fc', stroke: '#6c8ebf' },
green: { fill: '#d5e8d4', stroke: '#82b366' },
yellow: { fill: '#fff2cc', stroke: '#d6b656' },
red: { fill: '#f8cecc', stroke: '#E74C3C' },
orange: { fill: '#ffe6cc', stroke: '#d79b00' }
};
/**
* 形状预设
*/
const SHAPE_PRESETS = {
rect: 'rounded=1;whiteSpace=wrap;html=1',
ellipse: 'ellipse;whiteSpace=wrap;html=1',
diamond: 'rhombus;whiteSpace=wrap;html=1',
circle: 'ellipse;aspect=fixed;whiteSpace=wrap;html=1',
cylinder: 'shape=cylinder3;whiteSpace=wrap;html=1',
hexagon: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1'
};
/**
* 解析 DrawioLite DSL
* @param {string} dsl - DSL 文本
* @returns {Object} { pages, hasMultiPage, nodes, edges, groups, subgraphs, legend }
*/
function parseDrawioLite(dsl) {
// 边界检查
if (!dsl || typeof dsl !== 'string') {
console.warn('[DrawioLite] 解析失败:输入为空或非字符串');
return {
pages: [],
hasMultiPage: false,
nodes: [],
edges: [],
groups: [],
subgraphs: [],
legend: []
};
}
const lines = dsl.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
// 全局容器
const result = {
pages: [],
hasMultiPage: false,
nodes: [],
edges: [],
groups: [],
subgraphs: [],
legend: []
};
// 状态追踪
let currentContext = result; // 当前上下文(全局、页面、子图)
let inGroup = null;
let inLegend = false;
const contextStack = []; // 上下文栈
const seenNodeIds = new Set(); // 节点 ID 去重
for (let line of lines) {
// 1. 多页:page "标题" {
const pageMatch = line.match(/^page\s+"([^"]+)"\s*\{/);
if (pageMatch) {
result.hasMultiPage = true;
const page = {
title: pageMatch[1],
nodes: [],
edges: [],
groups: [],
subgraphs: [],
legend: []
};
result.pages.push(page);
contextStack.push(currentContext);
currentContext = page;
seenNodeIds.clear(); // 重置节点 ID 追踪(每个页面有独立的命名空间)
continue;
}
// 2. 子图:subgraph ID "标题" {
const subgraphMatch = line.match(/^subgraph\s+(\w+)\s+"([^"]+)"\s*\{/);
if (subgraphMatch) {
const [, id, title] = subgraphMatch;
const subgraph = { id, title, nodes: [], edges: [] };
currentContext.subgraphs.push(subgraph);
contextStack.push(currentContext);
currentContext = subgraph;
continue;
}
// 3. 分组:group ID "标题" {
const groupMatch = line.match(/^group\s+(\w+)\s+"([^"]+)"\s*\{/);
if (groupMatch) {
const [, id, title] = groupMatch;
inGroup = { id, title, members: [] };
continue;
}
// 4. 图例:legend {
if (line === 'legend {') {
inLegend = true;
continue;
}
// 5. 闭合:}
if (line === '}') {
if (inGroup) {
currentContext.groups.push(inGroup);
inGroup = null;
} else if (inLegend) {
inLegend = false;
} else if (contextStack.length > 0) {
currentContext = contextStack.pop();
}
continue;
}
// 6. 节点:node ID "文本" 形状 [颜色]
const nodeMatch = line.match(/^node\s+(\w+)\s+"([^"]*)"\s+(\w+)(?:\s+(\w+))?/);
if (nodeMatch) {
const [, id, label, shape, color = 'gray'] = nodeMatch;
// 检测 ID 重复
if (seenNodeIds.has(id)) {
console.warn(`[DrawioLite] 警告:节点 ID "${id}" 重复,可能导致连接错误`);
}
seenNodeIds.add(id);
// 检测空标签
if (!label || label.trim() === '') {
console.warn(`[DrawioLite] 警告:节点 "${id}" 标签为空`);
}
currentContext.nodes.push({ id, label: label || id, shape, color });
continue;
}
// 7. 连接:A -> B ["标签"] 或 S1.A -> S2.B "跨子图"
const edgeMatch = line.match(/^([\w.]+)\s*->\s*([\w.]+)(?:\s+"([^"]+)")?/);
if (edgeMatch) {
const [, from, to, label = ''] = edgeMatch;
currentContext.edges.push({ from, to, label });
continue;
}
// 8. 分组成员:A, B, C
if (inGroup && line.match(/^[\w\s,]+$/)) {
const members = line.split(',').map(m => m.trim()).filter(m => m);
inGroup.members.push(...members);
continue;
}
// 9. 图例项:形状 颜色 "说明"
if (inLegend) {
const legendMatch = line.match(/^(\w+)\s+(\w+)\s+"([^"]+)"/);
if (legendMatch) {
const [, shape, color, text] = legendMatch;
currentContext.legend.push({ shape, color, text });
}
continue;
}
}
return result;
}
/**
* 生成单个图表页面的 XML
*/
function generatePageXml(pageData, pageId, cellIdStart) {
const { nodes, edges, groups, subgraphs, legend } = pageData;
let cellId = cellIdStart;
const nodeIdMap = new Map();
let xml = '';
// 0. 预处理:识别哪些节点属于 group
const nodeToGroup = new Map(); // 节点ID -> group ID
groups.forEach(group => {
group.members.forEach(memberId => {
nodeToGroup.set(memberId, group.id);
});
});
// 1. 处理子图(并列布局)
if (subgraphs.length > 0) {
let offsetX = 50;
subgraphs.forEach(subgraph => {
// 子图容器
const containerCellId = cellId++;
xml += `
`;
// 子图内的节点
let nodeY = 80;
subgraph.nodes.forEach(node => {
const { id, label, shape, color } = node;
// 颜色 fallback 检查
if (!COLOR_PRESETS[color]) {
console.warn(`[DrawioLite] 未知颜色 "${color}",使用默认 gray`);
}
const colors = COLOR_PRESETS[color] || COLOR_PRESETS.gray;
// 形状 fallback 检查
if (!SHAPE_PRESETS[shape]) {
console.warn(`[DrawioLite] 未知形状 "${shape}",使用默认 rect`);
}
const shapeStyle = SHAPE_PRESETS[shape] || SHAPE_PRESETS.rect;
const style = `${shapeStyle};fillColor=${colors.fill};strokeColor=${colors.stroke};strokeWidth=2;fontSize=12;fontFamily=Arial;`;
const mxCellId = cellId++;
nodeIdMap.set(`${subgraph.id}.${id}`, mxCellId);
nodeIdMap.set(id, mxCellId); // 也支持简写
const width = shape === 'diamond' ? 140 : 120;
const height = shape === 'diamond' ? 100 : 60;
xml += `
`;
nodeY += height + 60;
});
// 子图内的连接
subgraph.edges.forEach(edge => {
const { from, to, label } = edge;
const sourceId = nodeIdMap.get(from) || nodeIdMap.get(`${subgraph.id}.${from}`);
const targetId = nodeIdMap.get(to) || nodeIdMap.get(`${subgraph.id}.${to}`);
if (!sourceId || !targetId) {
console.warn(`[DrawioLite] 跳过无效连接: ${from} -> ${to}`);
return;
}
const mxCellId = cellId++;
const style = 'edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#2C3E50;strokeWidth=2;fontSize=10;fontFamily=Arial;endArrow=classicBlock;';
xml += `
`;
});
offsetX += 350; // 下一个子图的横向偏移
});
} else {
// 2. 普通节点(无子图)- 使用改进的初始布局
// 2.1 先创建 group 容器(如果有)
const groupContainers = new Map(); // group ID -> 容器 mxCell ID
groups.forEach(group => {
const containerCellId = cellId++;
groupContainers.set(group.id, containerCellId);
// 创建 group 容器(初始尺寸,后面会调整)
xml += `
`;
});
// 2.2 渲染节点
let nodeX = 80; // 初始 X 坐标
let nodeY = 80; // 初始 Y 坐标
const columnWidth = 200; // 每列的宽度
const rowHeight = 140; // 每行的高度
const nodesPerRow = 4; // 每行最多节点数
// 按 group 分组节点
const groupedNodes = new Map(); // group ID -> nodes[]
const ungroupedNodes = [];
nodes.forEach(node => {
const groupId = nodeToGroup.get(node.id);
if (groupId) {
if (!groupedNodes.has(groupId)) {
groupedNodes.set(groupId, []);
}
groupedNodes.get(groupId).push(node);
} else {
ungroupedNodes.push(node);
}
});
// 2.3 渲染不属于 group 的节点
ungroupedNodes.forEach((node, index) => {
const { id, label, shape, color } = node;
// 颜色 fallback 检查
if (!COLOR_PRESETS[color]) {
console.warn(`[DrawioLite] 未知颜色 "${color}",使用默认 gray`);
}
const colors = COLOR_PRESETS[color] || COLOR_PRESETS.gray;
// 形状 fallback 检查
if (!SHAPE_PRESETS[shape]) {
console.warn(`[DrawioLite] 未知形状 "${shape}",使用默认 rect`);
}
const shapeStyle = SHAPE_PRESETS[shape] || SHAPE_PRESETS.rect;
const style = `${shapeStyle};fillColor=${colors.fill};strokeColor=${colors.stroke};strokeWidth=2;fontSize=12;fontFamily=Arial;`;
const mxCellId = cellId++;
nodeIdMap.set(id, mxCellId);
const width = shape === 'diamond' ? 140 : 120;
const height = shape === 'diamond' ? 100 : 60;
// 计算当前节点的位置(网格布局)
const row = Math.floor(index / nodesPerRow);
const col = index % nodesPerRow;
const x = nodeX + col * columnWidth;
const y = nodeY + row * rowHeight;
xml += `
`;
});
// 2.4 渲染属于 group 的节点(相对于容器坐标)
groups.forEach(group => {
const groupNodes = groupedNodes.get(group.id) || [];
const containerCellId = groupContainers.get(group.id);
let groupNodeY = 60; // 起始Y(容器内相对坐标,标题下方)
groupNodes.forEach(node => {
const { id, label, shape, color } = node;
// 颜色 fallback 检查
if (!COLOR_PRESETS[color]) {
console.warn(`[DrawioLite] 未知颜色 "${color}",使用默认 gray`);
}
const colors = COLOR_PRESETS[color] || COLOR_PRESETS.gray;
// 形状 fallback 检查
if (!SHAPE_PRESETS[shape]) {
console.warn(`[DrawioLite] 未知形状 "${shape}",使用默认 rect`);
}
const shapeStyle = SHAPE_PRESETS[shape] || SHAPE_PRESETS.rect;
const style = `${shapeStyle};fillColor=${colors.fill};strokeColor=${colors.stroke};strokeWidth=2;fontSize=12;fontFamily=Arial;`;
const mxCellId = cellId++;
nodeIdMap.set(id, mxCellId);
const width = shape === 'diamond' ? 140 : 120;
const height = shape === 'diamond' ? 100 : 60;
xml += `
`;
groupNodeY += height + 60;
});
});
// 3. 连接(包括跨子图连接)
edges.forEach(edge => {
const { from, to, label } = edge;
const sourceId = nodeIdMap.get(from);
const targetId = nodeIdMap.get(to);
if (!sourceId || !targetId) {
console.warn(`[DrawioLite] 跳过无效连接: ${from} -> ${to}`);
return;
}
const mxCellId = cellId++;
const style = 'edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#2C3E50;strokeWidth=2;fontSize=10;fontFamily=Arial;endArrow=classicBlock;';
xml += `
`;
});
}
// 4. 图例 - 动态计算位置避免重叠
if (legend.length > 0) {
// 估算图表的最大范围
let maxX = 0;
if (subgraphs.length > 0) {
// 有子图:每个子图宽度约350px
maxX = 100 + subgraphs.length * 350;
} else {
// 网格布局:4列 * 200px列宽 + 节点宽度 + 起始X
const numNodes = nodes.length;
const nodesPerRow = 4;
const columnWidth = 200;
const maxNodeWidth = 140; // diamond 最宽
maxX = 80 + Math.min(numNodes, nodesPerRow) * columnWidth + maxNodeWidth;
}
// 图例放在图表右侧,留100px间距
const legendX = maxX + 100;
const legendY = 80; // 与节点起始Y对齐
legend.forEach((item, index) => {
const { shape, color, text } = item;
// 图例项验证
if (!COLOR_PRESETS[color]) {
console.warn(`[DrawioLite] 图例中未知颜色 "${color}",使用默认 gray`);
}
if (!SHAPE_PRESETS[shape]) {
console.warn(`[DrawioLite] 图例中未知形状 "${shape}",使用默认 rect`);
}
const colors = COLOR_PRESETS[color] || COLOR_PRESETS.gray;
const shapeStyle = SHAPE_PRESETS[shape] || SHAPE_PRESETS.rect;
const yOffset = index * 40;
const shapeCellId = cellId++;
xml += `
`;
const textCellId = cellId++;
xml += `
`;
});
}
return { xml, nextCellId: cellId };
}
/**
* 将 DrawioLite AST 转换为 Draw.io XML
*/
function drawioLiteToXml(ast) {
const { pages, hasMultiPage } = ast;
let xml = '\n';
let cellId = 2;
if (hasMultiPage && pages.length > 0) {
// 多页模式
pages.forEach((page, index) => {
const pageId = `page-${index + 1}`;
xml += `
`;
const result = generatePageXml(page, '1', cellId);
xml += result.xml;
cellId = result.nextCellId;
xml += `
`;
});
} else {
// 单页模式
xml += `
`;
const result = generatePageXml(ast, '1', cellId);
xml += result.xml;
xml += `
`;
}
xml += '';
return xml;
}
/**
* XML 特殊字符转义
*/
function escapeXml(str) {
if (!str) return '';
return str
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* 验证 AST 的有效性
* @param {Object} ast - 解析后的抽象语法树
* @returns {Object} { valid: boolean, warnings: string[] }
*/
function validateAST(ast) {
const warnings = [];
// 检查是否有内容(包括多页图表)
let totalNodes = ast.nodes.length + ast.subgraphs.reduce((sum, sg) => sum + sg.nodes.length, 0);
let totalEdges = ast.edges.length + ast.subgraphs.reduce((sum, sg) => sum + sg.edges.length, 0);
// 如果是多页图表,统计所有页面的节点和边
if (ast.hasMultiPage && ast.pages) {
ast.pages.forEach(page => {
totalNodes += page.nodes.length + page.subgraphs.reduce((sum, sg) => sum + sg.nodes.length, 0);
totalEdges += page.edges.length + page.subgraphs.reduce((sum, sg) => sum + sg.edges.length, 0);
});
}
if (totalNodes === 0) {
warnings.push('图表中没有节点');
}
if (totalEdges === 0 && totalNodes > 1) {
warnings.push('图表中没有连接线,节点可能孤立');
}
// 检查连接密度(对多页图表放宽要求)
if (totalNodes > 0) {
const ratio = totalEdges / totalNodes;
const threshold = ast.hasMultiPage ? 0.1 : 0.3; // 多页图表连接密度要求更低
if (ratio < threshold) {
warnings.push(`连接密度过低 (${ratio.toFixed(2)}),建议增加连接`);
}
}
return { valid: warnings.length === 0, warnings };
}
/**
* 主转换函数:DrawioLite → Draw.io XML(带自动布局)
*/
function convertDrawioLite(dslText) {
try {
console.log('[DrawioLite] 🎯 开始解析 DSL...');
const ast = parseDrawioLite(dslText);
console.log('[DrawioLite] ✅ 解析完成:', ast);
// 验证 AST
const validation = validateAST(ast);
if (validation.warnings.length > 0) {
console.warn('[DrawioLite] ⚠️ 发现问题:', validation.warnings.join('; '));
}
let xml = drawioLiteToXml(ast);
console.log('[DrawioLite] ✅ XML 生成完成');
// 应用 Dagre 自动布局(现已支持多页图表)
if (window.DrawioLayoutOptimizer) {
console.log('[DrawioLite] 🎨 应用自动布局优化(多页支持)...');
xml = window.DrawioLayoutOptimizer.optimizeDrawioLayout(xml, {
dagreLayout: true, // 使用 Dagre 算法
gridAlignment: true, // 网格对齐
connections: true, // 连接优化
spacing: false, // 禁用间距优化(Dagre 已处理)
styles: false, // 保留 DSL 定义的颜色
layoutDirection: 'TB' // 使用 TB(从上到下)布局,更紧凑
});
console.log('[DrawioLite] ✅ 布局优化完成');
} else {
console.warn('[DrawioLite] ⚠️ DrawioLayoutOptimizer 未加载,跳过布局优化');
}
return xml;
} catch (error) {
console.error('[DrawioLite] ❌ 转换失败:', error);
throw error;
}
}
// 导出到全局
window.DrawioLiteParser = {
parseDrawioLite,
drawioLiteToXml,
convertDrawioLite
};
console.log('[DrawioLite] ✅ Parser 已加载(v1.1.0 - 支持复杂多图)');