mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-09 17:19:45 +02:00
testing with antlr code generation
This commit is contained in:
331
demos/class-antlr-test.html
Normal file
331
demos/class-antlr-test.html
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<title>Mermaid Class ANTLR Parser Test Page</title>
|
||||||
|
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
margin: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-section {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.parser-info {
|
||||||
|
background: #e3f2fd;
|
||||||
|
border: 1px solid #2196f3;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
background: #e8f5e8;
|
||||||
|
border: 1px solid #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #ffebee;
|
||||||
|
border: 1px solid #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.broken {
|
||||||
|
background: #fff3e0;
|
||||||
|
border: 1px solid #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.mermaid {
|
||||||
|
font-family: 'Courier New', Courier, monospace !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #424242;
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#debug-logs {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagram-code {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>🎯 Mermaid Class ANTLR Parser Test Page</h1>
|
||||||
|
|
||||||
|
<div class="parser-info">
|
||||||
|
<h3>🔧 Parser Information</h3>
|
||||||
|
<p><strong>Environment Variable:</strong> <code id="env-var">Loading...</code></p>
|
||||||
|
<p><strong>Expected:</strong> <code>USE_ANTLR_PARSER=true</code></p>
|
||||||
|
<p><strong>Status:</strong> <span id="parser-status">Checking...</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>Test 1: Simple Class Diagram</h2>
|
||||||
|
<p>Basic class diagram to test ANTLR parser functionality:</p>
|
||||||
|
<pre class="mermaid">
|
||||||
|
classDiagram
|
||||||
|
class Animal {
|
||||||
|
+name: string
|
||||||
|
+age: int
|
||||||
|
+makeSound()
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>Test 2: Class with Relationships</h2>
|
||||||
|
<p>Testing class relationships:</p>
|
||||||
|
<pre class="mermaid">
|
||||||
|
classDiagram
|
||||||
|
class Animal {
|
||||||
|
+name: string
|
||||||
|
+makeSound()
|
||||||
|
}
|
||||||
|
class Dog {
|
||||||
|
+breed: string
|
||||||
|
+bark()
|
||||||
|
}
|
||||||
|
Animal <|-- Dog
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section broken">
|
||||||
|
<h2>🚨 Test 3: BROKEN DIAGRAM - Debug Target</h2>
|
||||||
|
<p><strong>This is the problematic diagram that needs debugging:</strong></p>
|
||||||
|
<div class="diagram-code">classDiagram
|
||||||
|
class Person {
|
||||||
|
+ID : Guid
|
||||||
|
+FirstName : string
|
||||||
|
+LastName : string
|
||||||
|
-privateProperty : string
|
||||||
|
#ProtectedProperty : string
|
||||||
|
~InternalProperty : string
|
||||||
|
~AnotherInternalProperty : List~List~string~~
|
||||||
|
}
|
||||||
|
class People List~List~Person~~</div>
|
||||||
|
<p><strong>Expected Error:</strong> Parse error on line 11: Expecting 'STR'</p>
|
||||||
|
<pre class="mermaid">
|
||||||
|
classDiagram
|
||||||
|
class Person {
|
||||||
|
+ID : Guid
|
||||||
|
+FirstName : string
|
||||||
|
+LastName : string
|
||||||
|
-privateProperty : string
|
||||||
|
#ProtectedProperty : string
|
||||||
|
~InternalProperty : string
|
||||||
|
~AnotherInternalProperty : List~List~string~~
|
||||||
|
}
|
||||||
|
class People List~List~Person~~
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>Test 4: Generic Types (Simplified)</h2>
|
||||||
|
<p>Testing simpler generic type syntax:</p>
|
||||||
|
<pre class="mermaid">
|
||||||
|
classDiagram
|
||||||
|
class Person {
|
||||||
|
+ID : Guid
|
||||||
|
+FirstName : string
|
||||||
|
+LastName : string
|
||||||
|
}
|
||||||
|
class People {
|
||||||
|
+items : List~Person~
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>Test 5: Visibility Modifiers</h2>
|
||||||
|
<p>Testing different visibility modifiers:</p>
|
||||||
|
<pre class="mermaid">
|
||||||
|
classDiagram
|
||||||
|
class TestClass {
|
||||||
|
+publicField : string
|
||||||
|
-privateField : string
|
||||||
|
#protectedField : string
|
||||||
|
~packageField : string
|
||||||
|
+publicMethod()
|
||||||
|
-privateMethod()
|
||||||
|
#protectedMethod()
|
||||||
|
~packageMethod()
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import mermaid from './mermaid.esm.mjs';
|
||||||
|
|
||||||
|
// Configure ANTLR parser for browser environment
|
||||||
|
window.MERMAID_CONFIG = {
|
||||||
|
USE_ANTLR_PARSER: 'true',
|
||||||
|
USE_ANTLR_VISITOR: 'false', // Use listener pattern
|
||||||
|
ANTLR_DEBUG: 'true'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🎯 Class ANTLR Configuration:', window.MERMAID_CONFIG);
|
||||||
|
|
||||||
|
// Override console methods to capture logs
|
||||||
|
const originalLog = console.log;
|
||||||
|
const originalError = console.error;
|
||||||
|
|
||||||
|
function createLogDiv() {
|
||||||
|
const logDiv = document.createElement('div');
|
||||||
|
logDiv.id = 'debug-logs';
|
||||||
|
logDiv.innerHTML = '<h3>🔍 Debug Logs:</h3>';
|
||||||
|
document.body.appendChild(logDiv);
|
||||||
|
return logDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log = function (...args) {
|
||||||
|
originalLog.apply(console, args);
|
||||||
|
// Display important logs on page
|
||||||
|
if (args[0] && typeof args[0] === 'string' && (
|
||||||
|
args[0].includes('ANTLR') ||
|
||||||
|
args[0].includes('ClassDB:') ||
|
||||||
|
args[0].includes('ClassListener:') ||
|
||||||
|
args[0].includes('ClassVisitor:') ||
|
||||||
|
args[0].includes('ClassParserCore:') ||
|
||||||
|
args[0].includes('Class ANTLR') ||
|
||||||
|
args[0].includes('🔧') ||
|
||||||
|
args[0].includes('❌') ||
|
||||||
|
args[0].includes('✅')
|
||||||
|
)) {
|
||||||
|
const logDiv = document.getElementById('debug-logs') || createLogDiv();
|
||||||
|
logDiv.innerHTML += '<div style="color: blue; margin: 2px 0;">' + args.join(' ') + '</div>';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.error = function (...args) {
|
||||||
|
originalError.apply(console, args);
|
||||||
|
const logDiv = document.getElementById('debug-logs') || createLogDiv();
|
||||||
|
logDiv.innerHTML += '<div style="color: red; margin: 2px 0;">ERROR: ' + args.join(' ') + '</div>';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize mermaid
|
||||||
|
mermaid.initialize({
|
||||||
|
theme: 'default',
|
||||||
|
logLevel: 3,
|
||||||
|
securityLevel: 'loose',
|
||||||
|
class: {
|
||||||
|
titleTopMargin: 25,
|
||||||
|
diagramPadding: 50,
|
||||||
|
htmlLabels: false
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check environment and parser status
|
||||||
|
let envVar = 'undefined';
|
||||||
|
try {
|
||||||
|
if (typeof process !== 'undefined' && process.env) {
|
||||||
|
envVar = process.env.USE_ANTLR_PARSER || 'undefined';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
envVar = 'browser-default';
|
||||||
|
}
|
||||||
|
|
||||||
|
const envElement = document.getElementById('env-var');
|
||||||
|
const statusElement = document.getElementById('parser-status');
|
||||||
|
|
||||||
|
if (envElement) {
|
||||||
|
envElement.textContent = `USE_ANTLR_PARSER=${envVar || 'undefined'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for debug information from parser
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.MERMAID_PARSER_DEBUG) {
|
||||||
|
console.log('🔍 Found MERMAID_PARSER_DEBUG:', window.MERMAID_PARSER_DEBUG);
|
||||||
|
const debug = window.MERMAID_PARSER_DEBUG;
|
||||||
|
|
||||||
|
if (envElement) {
|
||||||
|
envElement.textContent = `USE_ANTLR_PARSER=${debug.env_value || 'undefined'} (actual: ${debug.USE_ANTLR_PARSER})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusElement) {
|
||||||
|
if (debug.USE_ANTLR_PARSER) {
|
||||||
|
statusElement.innerHTML = '<span style="color: green;">✅ ANTLR Parser Active</span>';
|
||||||
|
statusElement.parentElement.parentElement.classList.add('success');
|
||||||
|
} else {
|
||||||
|
statusElement.innerHTML = '<span style="color: orange;">⚠️ Jison Parser (Default)</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
if (statusElement) {
|
||||||
|
if (envVar === 'true') {
|
||||||
|
statusElement.innerHTML = '<span style="color: green;">✅ ANTLR Parser Active</span>';
|
||||||
|
statusElement.parentElement.parentElement.classList.add('success');
|
||||||
|
} else {
|
||||||
|
statusElement.innerHTML = '<span style="color: orange;">⚠️ Jison Parser (Default)</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add debugging
|
||||||
|
console.log('🎯 Class ANTLR Parser Test Page Loaded');
|
||||||
|
console.log('🔧 Environment:', { USE_ANTLR_PARSER: envVar });
|
||||||
|
|
||||||
|
// Test if we can detect which parser is being used
|
||||||
|
setTimeout(() => {
|
||||||
|
const mermaidElements = document.querySelectorAll('.mermaid');
|
||||||
|
console.log(`📊 Found ${mermaidElements.length} class diagrams`);
|
||||||
|
|
||||||
|
// Check if diagrams rendered successfully
|
||||||
|
const renderedElements = document.querySelectorAll('.mermaid svg');
|
||||||
|
if (renderedElements.length > 0) {
|
||||||
|
console.log('✅ Class diagrams rendered successfully!');
|
||||||
|
console.log(`📈 ${renderedElements.length} SVG elements created`);
|
||||||
|
|
||||||
|
// Update status on page
|
||||||
|
const statusElement = document.getElementById('parser-status');
|
||||||
|
if (statusElement && envVar === 'true') {
|
||||||
|
statusElement.innerHTML = '<span style="color: green;">✅ ANTLR Parser Active & Rendering Successfully!</span>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('❌ No SVG elements found - check for rendering errors');
|
||||||
|
console.log('🔍 Checking for error messages...');
|
||||||
|
|
||||||
|
// Look for error messages in mermaid elements
|
||||||
|
mermaidElements.forEach((element, index) => {
|
||||||
|
console.log(`📋 Class Diagram ${index + 1} content:`, element.textContent.trim());
|
||||||
|
if (element.innerHTML.includes('error') || element.innerHTML.includes('Error')) {
|
||||||
|
console.log(`❌ Error found in class diagram ${index + 1}:`, element.innerHTML);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
358
demos/hybrid-sequence-test.html
Normal file
358
demos/hybrid-sequence-test.html
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>🚀 Hybrid Sequence Editor Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: #333;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: #4a5568;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-section {
|
||||||
|
margin: 30px 0;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-section h2 {
|
||||||
|
color: #2d3748;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #cbd5e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
background: #1a202c;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 15px 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f0fff4;
|
||||||
|
border-left: 4px solid #48bb78;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff5f5;
|
||||||
|
border-left: 4px solid #f56565;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #718096;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operations {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-card {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-card h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-output {
|
||||||
|
background: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🚀 Hybrid Sequence Editor Test</h1>
|
||||||
|
<p style="text-align: center; color: #718096; font-size: 1.1em;">
|
||||||
|
Testing the new hybrid approach: AST-based editing + TokenStreamRewriter for optimal performance
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Test Section 1: Basic Functionality -->
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>🎯 Basic Hybrid Editor Test</h2>
|
||||||
|
<div class="controls">
|
||||||
|
<button class="btn-primary" onclick="testBasicFunctionality()">Test Basic Functionality</button>
|
||||||
|
<button class="btn-secondary" onclick="clearResults()">Clear Results</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-block" id="originalCode">sequenceDiagram
|
||||||
|
Alice->>Bob: Hello Bob, how are you?
|
||||||
|
Bob-->>Alice: Great!</div>
|
||||||
|
|
||||||
|
<div id="basicResults"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test Section 2: CRUD Operations -->
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>✏️ CRUD Operations Test</h2>
|
||||||
|
<div class="operations">
|
||||||
|
<div class="operation-card">
|
||||||
|
<h3>Add Participant</h3>
|
||||||
|
<input type="text" id="participantId" placeholder="Participant ID (e.g., C)" />
|
||||||
|
<input type="text" id="participantAlias" placeholder="Alias (e.g., Charlie)" />
|
||||||
|
<button class="btn-primary" onclick="addParticipant()">Add Participant</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="operation-card">
|
||||||
|
<h3>Add Message</h3>
|
||||||
|
<input type="text" id="messageFrom" placeholder="From (e.g., Alice)" />
|
||||||
|
<input type="text" id="messageTo" placeholder="To (e.g., Bob)" />
|
||||||
|
<input type="text" id="messageText" placeholder="Message text" />
|
||||||
|
<select id="messageArrow">
|
||||||
|
<option value="->>">->></option>
|
||||||
|
<option value="-->>">-->></option>
|
||||||
|
<option value="->">-></option>
|
||||||
|
<option value="-->">--></option>
|
||||||
|
</select>
|
||||||
|
<button class="btn-primary" onclick="addMessage()">Add Message</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="operation-card">
|
||||||
|
<h3>Add Note</h3>
|
||||||
|
<select id="notePosition">
|
||||||
|
<option value="right">right</option>
|
||||||
|
<option value="left">left</option>
|
||||||
|
<option value="over">over</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="noteParticipant" placeholder="Participant (e.g., Bob)" />
|
||||||
|
<input type="text" id="noteText" placeholder="Note text" />
|
||||||
|
<button class="btn-primary" onclick="addNote()">Add Note</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="operation-card">
|
||||||
|
<h3>Move Statement</h3>
|
||||||
|
<input type="number" id="moveFrom" placeholder="From index" />
|
||||||
|
<input type="number" id="moveTo" placeholder="To index" />
|
||||||
|
<button class="btn-primary" onclick="moveStatement()">Move Statement</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button class="btn-primary" onclick="regenerateCode()">Regenerate Code</button>
|
||||||
|
<button class="btn-secondary" onclick="showAST()">Show AST</button>
|
||||||
|
<button class="btn-secondary" onclick="validateAST()">Validate AST</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="crudResults"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test Section 3: Performance Comparison -->
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>⚡ Performance Test</h2>
|
||||||
|
<div class="controls">
|
||||||
|
<button class="btn-primary" onclick="performanceTest()">Run Performance Test</button>
|
||||||
|
<select id="testSize">
|
||||||
|
<option value="small">Small (10 statements)</option>
|
||||||
|
<option value="medium">Medium (50 statements)</option>
|
||||||
|
<option value="large">Large (200 statements)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="performanceResults"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Debug Log -->
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>🔍 Debug Log</h2>
|
||||||
|
<div class="controls">
|
||||||
|
<button class="btn-secondary" onclick="clearLog()">Clear Log</button>
|
||||||
|
</div>
|
||||||
|
<div class="log-output" id="debugLog"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
// This will be implemented to test the hybrid editor
|
||||||
|
console.log('🚀 Hybrid Sequence Editor Test Page Loaded');
|
||||||
|
|
||||||
|
// Global variables for testing
|
||||||
|
let hybridEditor = null;
|
||||||
|
let currentAST = null;
|
||||||
|
|
||||||
|
// Test functions will be implemented here
|
||||||
|
window.testBasicFunctionality = function() {
|
||||||
|
log('🎯 Testing basic hybrid editor functionality...');
|
||||||
|
log('⚠️ Implementation pending - hybrid editor classes need to be imported');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addParticipant = function() {
|
||||||
|
const id = document.getElementById('participantId').value;
|
||||||
|
const alias = document.getElementById('participantAlias').value;
|
||||||
|
log(`👤 Adding participant: ${id}${alias ? ` as ${alias}` : ''}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addMessage = function() {
|
||||||
|
const from = document.getElementById('messageFrom').value;
|
||||||
|
const to = document.getElementById('messageTo').value;
|
||||||
|
const text = document.getElementById('messageText').value;
|
||||||
|
const arrow = document.getElementById('messageArrow').value;
|
||||||
|
log(`💬 Adding message: ${from}${arrow}${to}: ${text}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addNote = function() {
|
||||||
|
const position = document.getElementById('notePosition').value;
|
||||||
|
const participant = document.getElementById('noteParticipant').value;
|
||||||
|
const text = document.getElementById('noteText').value;
|
||||||
|
log(`📝 Adding note: Note ${position} of ${participant}: ${text}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.moveStatement = function() {
|
||||||
|
const from = document.getElementById('moveFrom').value;
|
||||||
|
const to = document.getElementById('moveTo').value;
|
||||||
|
log(`🔄 Moving statement from ${from} to ${to}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.regenerateCode = function() {
|
||||||
|
log('🔄 Regenerating code from AST...');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.showAST = function() {
|
||||||
|
log('🌳 Showing current AST structure...');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.validateAST = function() {
|
||||||
|
log('✅ Validating AST structure...');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.performanceTest = function() {
|
||||||
|
const size = document.getElementById('testSize').value;
|
||||||
|
log(`⚡ Running performance test with ${size} dataset...`);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearResults = function() {
|
||||||
|
document.getElementById('basicResults').innerHTML = '';
|
||||||
|
document.getElementById('crudResults').innerHTML = '';
|
||||||
|
document.getElementById('performanceResults').innerHTML = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearLog = function() {
|
||||||
|
document.getElementById('debugLog').innerHTML = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
const logElement = document.getElementById('debugLog');
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
logElement.innerHTML += `[${timestamp}] ${message}\n`;
|
||||||
|
logElement.scrollTop = logElement.scrollHeight;
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,317 @@
|
|||||||
|
import { CommonTokenStream, TokenStreamRewriter } from 'antlr4ng';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base interfaces for diagram editing
|
||||||
|
*/
|
||||||
|
export interface DiagramStatement {
|
||||||
|
type: string;
|
||||||
|
originalIndex: number;
|
||||||
|
data: any;
|
||||||
|
sourceTokens?: { start: any; stop: any }; // Reference to original tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagramAST {
|
||||||
|
header: string;
|
||||||
|
statements: DiagramStatement[];
|
||||||
|
metadata?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditOperation {
|
||||||
|
type: 'insert' | 'update' | 'delete' | 'move';
|
||||||
|
index: number;
|
||||||
|
data?: any;
|
||||||
|
targetIndex?: number; // for move operations
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for hybrid diagram editors
|
||||||
|
* Combines AST-based structural editing with TokenStreamRewriter for performance
|
||||||
|
*/
|
||||||
|
export abstract class HybridDiagramEditor<T extends DiagramAST> {
|
||||||
|
protected ast: T;
|
||||||
|
protected tokenRewriter: TokenStreamRewriter;
|
||||||
|
protected originalTokenStream: CommonTokenStream;
|
||||||
|
protected pendingOperations: EditOperation[] = [];
|
||||||
|
protected operationHistory: EditOperation[][] = []; // For undo/redo
|
||||||
|
|
||||||
|
constructor(protected input: string, protected diagramType: string) {
|
||||||
|
console.log(`🏗️ Initializing ${diagramType} hybrid editor`);
|
||||||
|
this.parseAndBuildAST();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse input and build both token stream and AST
|
||||||
|
*/
|
||||||
|
private parseAndBuildAST(): void {
|
||||||
|
try {
|
||||||
|
const { parser, tokenStream } = this.createParser(this.input);
|
||||||
|
this.originalTokenStream = tokenStream;
|
||||||
|
this.tokenRewriter = new TokenStreamRewriter(tokenStream);
|
||||||
|
|
||||||
|
console.log(`🌳 Building AST for ${this.diagramType}`);
|
||||||
|
this.ast = this.buildAST(parser);
|
||||||
|
|
||||||
|
console.log(`✅ ${this.diagramType} AST built successfully:`, {
|
||||||
|
statements: this.ast.statements.length,
|
||||||
|
header: this.ast.header
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Failed to parse ${this.diagramType}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract methods each diagram type must implement
|
||||||
|
*/
|
||||||
|
protected abstract createParser(input: string): { parser: any; tokenStream: CommonTokenStream };
|
||||||
|
protected abstract buildAST(parser: any): T;
|
||||||
|
protected abstract regenerateFromAST(): string;
|
||||||
|
protected abstract getStatementCount(): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current AST (read-only)
|
||||||
|
*/
|
||||||
|
getAST(): Readonly<T> {
|
||||||
|
return this.ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statement by index
|
||||||
|
*/
|
||||||
|
getStatement(index: number): DiagramStatement | undefined {
|
||||||
|
return this.ast.statements.find(stmt => stmt.originalIndex === index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all statements of a specific type
|
||||||
|
*/
|
||||||
|
getStatementsByType(type: string): DiagramStatement[] {
|
||||||
|
return this.ast.statements.filter(stmt => stmt.type === type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new statement at the specified position
|
||||||
|
*/
|
||||||
|
insertStatement(afterIndex: number, statement: Omit<DiagramStatement, 'originalIndex'>): void {
|
||||||
|
console.log(`📝 Inserting ${statement.type} statement after index ${afterIndex}`);
|
||||||
|
|
||||||
|
// Update indices of statements after insertion point
|
||||||
|
this.ast.statements.forEach(stmt => {
|
||||||
|
if (stmt.originalIndex > afterIndex) {
|
||||||
|
stmt.originalIndex++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const newStatement: DiagramStatement = {
|
||||||
|
...statement,
|
||||||
|
originalIndex: afterIndex + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find insertion position in array
|
||||||
|
const insertPos = this.ast.statements.findIndex(stmt => stmt.originalIndex > afterIndex + 1);
|
||||||
|
if (insertPos === -1) {
|
||||||
|
this.ast.statements.push(newStatement);
|
||||||
|
} else {
|
||||||
|
this.ast.statements.splice(insertPos, 0, newStatement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record operation
|
||||||
|
this.recordOperation({
|
||||||
|
type: 'insert',
|
||||||
|
index: afterIndex + 1,
|
||||||
|
data: statement,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing statement
|
||||||
|
*/
|
||||||
|
updateStatement(index: number, newData: Partial<any>): void {
|
||||||
|
console.log(`✏️ Updating statement at index ${index}`);
|
||||||
|
|
||||||
|
const statement = this.ast.statements.find(stmt => stmt.originalIndex === index);
|
||||||
|
if (!statement) {
|
||||||
|
console.warn(`⚠️ Statement at index ${index} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldData = { ...statement.data };
|
||||||
|
statement.data = { ...statement.data, ...newData };
|
||||||
|
|
||||||
|
// Record operation
|
||||||
|
this.recordOperation({
|
||||||
|
type: 'update',
|
||||||
|
index,
|
||||||
|
data: { old: oldData, new: statement.data },
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a statement
|
||||||
|
*/
|
||||||
|
removeStatement(index: number): void {
|
||||||
|
console.log(`🗑️ Removing statement at index ${index}`);
|
||||||
|
|
||||||
|
const stmtIndex = this.ast.statements.findIndex(stmt => stmt.originalIndex === index);
|
||||||
|
if (stmtIndex === -1) {
|
||||||
|
console.warn(`⚠️ Statement at index ${index} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedStatement = this.ast.statements[stmtIndex];
|
||||||
|
this.ast.statements.splice(stmtIndex, 1);
|
||||||
|
|
||||||
|
// Update indices of statements after removal
|
||||||
|
this.ast.statements.forEach(stmt => {
|
||||||
|
if (stmt.originalIndex > index) {
|
||||||
|
stmt.originalIndex--;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Record operation
|
||||||
|
this.recordOperation({
|
||||||
|
type: 'delete',
|
||||||
|
index,
|
||||||
|
data: removedStatement,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a statement from one position to another
|
||||||
|
*/
|
||||||
|
moveStatement(fromIndex: number, toIndex: number): void {
|
||||||
|
console.log(`🔄 Moving statement from index ${fromIndex} to ${toIndex}`);
|
||||||
|
|
||||||
|
if (fromIndex === toIndex) return;
|
||||||
|
|
||||||
|
const statement = this.ast.statements.find(stmt => stmt.originalIndex === fromIndex);
|
||||||
|
if (!statement) {
|
||||||
|
console.warn(`⚠️ Statement at index ${fromIndex} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from current position
|
||||||
|
this.removeStatement(fromIndex);
|
||||||
|
|
||||||
|
// Adjust target index if necessary
|
||||||
|
const adjustedToIndex = toIndex > fromIndex ? toIndex - 1 : toIndex;
|
||||||
|
|
||||||
|
// Insert at new position
|
||||||
|
this.insertStatement(adjustedToIndex, {
|
||||||
|
type: statement.type,
|
||||||
|
data: statement.data,
|
||||||
|
sourceTokens: statement.sourceTokens
|
||||||
|
});
|
||||||
|
|
||||||
|
// Record operation (override the individual remove/insert operations)
|
||||||
|
this.pendingOperations.pop(); // Remove insert
|
||||||
|
this.pendingOperations.pop(); // Remove delete
|
||||||
|
this.recordOperation({
|
||||||
|
type: 'move',
|
||||||
|
index: fromIndex,
|
||||||
|
targetIndex: toIndex,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smart code regeneration with automatic strategy selection
|
||||||
|
*/
|
||||||
|
regenerateCode(strategy: 'ast' | 'tokens' | 'auto' = 'auto'): string {
|
||||||
|
console.log(`🔄 Regenerating code using ${strategy} strategy`);
|
||||||
|
|
||||||
|
if (strategy === 'auto') {
|
||||||
|
strategy = this.chooseOptimalStrategy();
|
||||||
|
console.log(`🤖 Auto-selected strategy: ${strategy}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = strategy === 'tokens'
|
||||||
|
? this.regenerateUsingTokens()
|
||||||
|
: this.regenerateFromAST();
|
||||||
|
|
||||||
|
console.log(`✅ Code regenerated successfully (${result.split('\n').length} lines)`);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Failed to regenerate code using ${strategy} strategy:`, error);
|
||||||
|
|
||||||
|
// Fallback to AST if tokens fail
|
||||||
|
if (strategy === 'tokens') {
|
||||||
|
console.log('🔄 Falling back to AST regeneration');
|
||||||
|
return this.regenerateFromAST();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choose optimal regeneration strategy based on file size and changes
|
||||||
|
*/
|
||||||
|
protected chooseOptimalStrategy(): 'ast' | 'tokens' {
|
||||||
|
const fileSize = this.input.length;
|
||||||
|
const statementCount = this.getStatementCount();
|
||||||
|
const changeRatio = this.pendingOperations.length / Math.max(statementCount, 1);
|
||||||
|
|
||||||
|
const hasStructuralChanges = this.pendingOperations.some(op =>
|
||||||
|
op.type === 'insert' || op.type === 'delete' || op.type === 'move'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`📊 Strategy selection metrics:`, {
|
||||||
|
fileSize,
|
||||||
|
statementCount,
|
||||||
|
pendingOperations: this.pendingOperations.length,
|
||||||
|
changeRatio: changeRatio.toFixed(2),
|
||||||
|
hasStructuralChanges
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use tokens for large files with minimal text-only changes
|
||||||
|
if (fileSize > 10000 && changeRatio < 0.1 && !hasStructuralChanges) {
|
||||||
|
return 'tokens';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use AST for structural changes or smaller files
|
||||||
|
return 'ast';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate using TokenStreamRewriter (preserves original formatting)
|
||||||
|
*/
|
||||||
|
protected regenerateUsingTokens(): string {
|
||||||
|
// Apply pending token-level operations
|
||||||
|
// This would be implemented by subclasses for specific token manipulations
|
||||||
|
return this.tokenRewriter.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record an operation for history/undo functionality
|
||||||
|
*/
|
||||||
|
private recordOperation(operation: EditOperation): void {
|
||||||
|
this.pendingOperations.push(operation);
|
||||||
|
|
||||||
|
// Limit history size to prevent memory issues
|
||||||
|
if (this.pendingOperations.length > 1000) {
|
||||||
|
this.pendingOperations = this.pendingOperations.slice(-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get operation history for debugging
|
||||||
|
*/
|
||||||
|
getOperationHistory(): ReadonlyArray<EditOperation> {
|
||||||
|
return this.pendingOperations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all pending operations (useful after successful save)
|
||||||
|
*/
|
||||||
|
clearOperations(): void {
|
||||||
|
console.log(`🧹 Clearing ${this.pendingOperations.length} pending operations`);
|
||||||
|
this.pendingOperations = [];
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,324 @@
|
|||||||
|
import { CommonTokenStream } from 'antlr4ng';
|
||||||
|
import { HybridDiagramEditor } from './HybridDiagramEditor.js';
|
||||||
|
import {
|
||||||
|
SequenceAST,
|
||||||
|
SequenceStatement,
|
||||||
|
ParticipantData,
|
||||||
|
MessageData,
|
||||||
|
NoteData,
|
||||||
|
LoopData,
|
||||||
|
SequenceASTHelper
|
||||||
|
} from './SequenceAST.js';
|
||||||
|
import { createSequenceParser } from './antlr-parser.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hybrid editor specifically for sequence diagrams
|
||||||
|
* Combines AST-based editing with TokenStreamRewriter for optimal performance
|
||||||
|
*/
|
||||||
|
export class HybridSequenceEditor extends HybridDiagramEditor<SequenceAST> {
|
||||||
|
|
||||||
|
constructor(input: string) {
|
||||||
|
super(input, 'sequence');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create ANTLR parser for sequence diagrams
|
||||||
|
*/
|
||||||
|
protected createParser(input: string): { parser: any; tokenStream: CommonTokenStream } {
|
||||||
|
console.log('🔧 Creating sequence diagram parser');
|
||||||
|
return createSequenceParser(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build sequence-specific AST from parse tree
|
||||||
|
*/
|
||||||
|
protected buildAST(parser: any): SequenceAST {
|
||||||
|
console.log('🌳 Building sequence AST from parse tree');
|
||||||
|
|
||||||
|
const builder = new SequenceASTBuilder();
|
||||||
|
const parseTree = parser.start();
|
||||||
|
|
||||||
|
// Visit the parse tree to build our AST
|
||||||
|
builder.visit(parseTree);
|
||||||
|
|
||||||
|
const ast = builder.getAST();
|
||||||
|
console.log('✅ Sequence AST built:', SequenceASTHelper.getStatistics(ast));
|
||||||
|
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate sequence diagram code from AST
|
||||||
|
*/
|
||||||
|
protected regenerateFromAST(): string {
|
||||||
|
console.log('🔄 Regenerating sequence code from AST');
|
||||||
|
|
||||||
|
let code = this.ast.header + '\n';
|
||||||
|
|
||||||
|
// Sort statements by original index to maintain order
|
||||||
|
const sortedStatements = [...this.ast.statements]
|
||||||
|
.sort((a, b) => a.originalIndex - b.originalIndex);
|
||||||
|
|
||||||
|
for (const stmt of sortedStatements) {
|
||||||
|
const line = this.generateStatementCode(stmt);
|
||||||
|
if (line) {
|
||||||
|
code += ' ' + line + '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return code.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate code for a single statement
|
||||||
|
*/
|
||||||
|
private generateStatementCode(stmt: SequenceStatement): string {
|
||||||
|
switch (stmt.type) {
|
||||||
|
case 'participant':
|
||||||
|
const p = stmt.data as ParticipantData;
|
||||||
|
return p.alias ? `participant ${p.id} as ${p.alias}` : `participant ${p.id}`;
|
||||||
|
|
||||||
|
case 'message':
|
||||||
|
const m = stmt.data as MessageData;
|
||||||
|
return `${m.from}${m.arrow}${m.to}: ${m.message}`;
|
||||||
|
|
||||||
|
case 'note':
|
||||||
|
const n = stmt.data as NoteData;
|
||||||
|
return `Note ${n.position} of ${n.participant}: ${n.message}`;
|
||||||
|
|
||||||
|
case 'activate':
|
||||||
|
return `activate ${(stmt.data as any).participant}`;
|
||||||
|
|
||||||
|
case 'deactivate':
|
||||||
|
return `deactivate ${(stmt.data as any).participant}`;
|
||||||
|
|
||||||
|
case 'loop':
|
||||||
|
const l = stmt.data as LoopData;
|
||||||
|
// For now, simplified loop handling - would need more complex logic for nested statements
|
||||||
|
return `loop ${l.condition}`;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`⚠️ Unknown statement type: ${stmt.type}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statement count for strategy selection
|
||||||
|
*/
|
||||||
|
protected getStatementCount(): number {
|
||||||
|
return this.ast.statements.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// High-level sequence diagram operations
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new participant
|
||||||
|
*/
|
||||||
|
addParticipant(id: string, alias?: string, afterIndex?: number): void {
|
||||||
|
console.log(`👤 Adding participant: ${id}${alias ? ` as ${alias}` : ''}`);
|
||||||
|
|
||||||
|
// Check if participant already exists
|
||||||
|
if (SequenceASTHelper.findParticipant(this.ast, id)) {
|
||||||
|
console.warn(`⚠️ Participant ${id} already exists`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const participantData: ParticipantData = { id, alias };
|
||||||
|
|
||||||
|
// If no position specified, add at the beginning (common pattern)
|
||||||
|
const insertIndex = afterIndex ?? -1;
|
||||||
|
|
||||||
|
this.insertStatement(insertIndex, {
|
||||||
|
type: 'participant',
|
||||||
|
data: participantData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update participant alias
|
||||||
|
*/
|
||||||
|
updateParticipantAlias(participantId: string, newAlias: string): void {
|
||||||
|
console.log(`✏️ Updating participant ${participantId} alias to: ${newAlias}`);
|
||||||
|
|
||||||
|
const stmt = this.ast.statements.find(s =>
|
||||||
|
s.type === 'participant' && (s.data as ParticipantData).id === participantId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!stmt) {
|
||||||
|
console.warn(`⚠️ Participant ${participantId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateStatement(stmt.originalIndex, { alias: newAlias });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new message
|
||||||
|
*/
|
||||||
|
addMessage(from: string, to: string, message: string, arrow: string = '->>', afterIndex?: number): void {
|
||||||
|
console.log(`💬 Adding message: ${from}${arrow}${to}: ${message}`);
|
||||||
|
|
||||||
|
const messageData: MessageData = { from, to, arrow, message };
|
||||||
|
|
||||||
|
// If no position specified, add at the end
|
||||||
|
const insertIndex = afterIndex ?? this.getLastStatementIndex();
|
||||||
|
|
||||||
|
this.insertStatement(insertIndex, {
|
||||||
|
type: 'message',
|
||||||
|
data: messageData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update message text
|
||||||
|
*/
|
||||||
|
updateMessageText(messageIndex: number, newText: string): void {
|
||||||
|
console.log(`✏️ Updating message at index ${messageIndex} to: ${newText}`);
|
||||||
|
|
||||||
|
const stmt = this.getStatement(messageIndex);
|
||||||
|
if (!stmt || stmt.type !== 'message') {
|
||||||
|
console.warn(`⚠️ Message at index ${messageIndex} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateStatement(messageIndex, { message: newText });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a note
|
||||||
|
*/
|
||||||
|
addNote(position: 'left' | 'right' | 'over', participant: string, message: string, afterIndex?: number): void {
|
||||||
|
console.log(`📝 Adding note: Note ${position} of ${participant}: ${message}`);
|
||||||
|
|
||||||
|
const noteData: NoteData = { position, participant, message };
|
||||||
|
|
||||||
|
const insertIndex = afterIndex ?? this.getLastStatementIndex();
|
||||||
|
|
||||||
|
this.insertStatement(insertIndex, {
|
||||||
|
type: 'note',
|
||||||
|
data: noteData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add activation
|
||||||
|
*/
|
||||||
|
addActivation(participant: string, afterIndex?: number): void {
|
||||||
|
console.log(`⚡ Adding activation for: ${participant}`);
|
||||||
|
|
||||||
|
const insertIndex = afterIndex ?? this.getLastStatementIndex();
|
||||||
|
|
||||||
|
this.insertStatement(insertIndex, {
|
||||||
|
type: 'activate',
|
||||||
|
data: { participant }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add deactivation
|
||||||
|
*/
|
||||||
|
addDeactivation(participant: string, afterIndex?: number): void {
|
||||||
|
console.log(`💤 Adding deactivation for: ${participant}`);
|
||||||
|
|
||||||
|
const insertIndex = afterIndex ?? this.getLastStatementIndex();
|
||||||
|
|
||||||
|
this.insertStatement(insertIndex, {
|
||||||
|
type: 'deactivate',
|
||||||
|
data: { participant }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap statements in a loop
|
||||||
|
*/
|
||||||
|
wrapInLoop(startIndex: number, endIndex: number, condition: string): void {
|
||||||
|
console.log(`🔄 Wrapping statements ${startIndex}-${endIndex} in loop: ${condition}`);
|
||||||
|
|
||||||
|
// This is a complex operation that would need careful implementation
|
||||||
|
// For now, just add a loop statement
|
||||||
|
const loopData: LoopData = { condition, statements: [] };
|
||||||
|
|
||||||
|
this.insertStatement(startIndex - 1, {
|
||||||
|
type: 'loop',
|
||||||
|
data: loopData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Helper methods
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the index of the last statement
|
||||||
|
*/
|
||||||
|
private getLastStatementIndex(): number {
|
||||||
|
if (this.ast.statements.length === 0) return -1;
|
||||||
|
return Math.max(...this.ast.statements.map(s => s.originalIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all participants (declared and mentioned)
|
||||||
|
*/
|
||||||
|
getAllParticipants(): Set<string> {
|
||||||
|
return SequenceASTHelper.getAllMentionedParticipants(this.ast);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sequence diagram statistics
|
||||||
|
*/
|
||||||
|
getStatistics() {
|
||||||
|
return SequenceASTHelper.getStatistics(this.ast);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the current AST
|
||||||
|
*/
|
||||||
|
validate() {
|
||||||
|
return SequenceASTHelper.validate(this.ast);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a summary of the diagram for debugging
|
||||||
|
*/
|
||||||
|
getSummary(): string {
|
||||||
|
const stats = this.getStatistics();
|
||||||
|
const participants = Array.from(this.getAllParticipants()).join(', ');
|
||||||
|
|
||||||
|
return `Sequence Diagram Summary:
|
||||||
|
- ${stats.totalStatements} total statements
|
||||||
|
- ${stats.participants} declared participants: ${participants}
|
||||||
|
- ${stats.messages} messages
|
||||||
|
- ${stats.notes} notes
|
||||||
|
- ${stats.loops} loops
|
||||||
|
- Complexity: ${stats.complexity}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AST Builder for sequence diagrams
|
||||||
|
* Converts ANTLR parse tree to our custom AST format
|
||||||
|
*/
|
||||||
|
class SequenceASTBuilder {
|
||||||
|
private ast: SequenceAST;
|
||||||
|
private currentIndex = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.ast = SequenceASTHelper.createEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAST(): SequenceAST {
|
||||||
|
return this.ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This would be implemented with proper visitor pattern
|
||||||
|
// For now, placeholder that would integrate with your existing SequenceCodeGenerator
|
||||||
|
visit(parseTree: any): void {
|
||||||
|
// TODO: Implement proper AST building from parse tree
|
||||||
|
// This would use the visitor pattern to traverse the parse tree
|
||||||
|
// and build the structured AST
|
||||||
|
console.log('🚧 AST building from parse tree - to be implemented');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,279 @@
|
|||||||
|
import { DiagramAST, DiagramStatement } from './HybridDiagramEditor.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sequence diagram specific AST interfaces
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ParticipantData {
|
||||||
|
id: string;
|
||||||
|
alias?: string;
|
||||||
|
displayName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageData {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
arrow: string; // ->>, -->, ->, etc.
|
||||||
|
message: string;
|
||||||
|
activate?: boolean;
|
||||||
|
deactivate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoopData {
|
||||||
|
condition: string;
|
||||||
|
statements: DiagramStatement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoteData {
|
||||||
|
position: 'left' | 'right' | 'over';
|
||||||
|
participant: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActivateData {
|
||||||
|
participant: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeactivateData {
|
||||||
|
participant: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AltData {
|
||||||
|
condition: string;
|
||||||
|
statements: DiagramStatement[];
|
||||||
|
elseStatements?: DiagramStatement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptData {
|
||||||
|
condition: string;
|
||||||
|
statements: DiagramStatement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParData {
|
||||||
|
statements: DiagramStatement[][];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RectData {
|
||||||
|
color?: string;
|
||||||
|
statements: DiagramStatement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sequence diagram statement types
|
||||||
|
*/
|
||||||
|
export type SequenceStatementType =
|
||||||
|
| 'participant'
|
||||||
|
| 'message'
|
||||||
|
| 'note'
|
||||||
|
| 'activate'
|
||||||
|
| 'deactivate'
|
||||||
|
| 'loop'
|
||||||
|
| 'alt'
|
||||||
|
| 'opt'
|
||||||
|
| 'par'
|
||||||
|
| 'rect'
|
||||||
|
| 'break'
|
||||||
|
| 'critical'
|
||||||
|
| 'autonumber';
|
||||||
|
|
||||||
|
export interface SequenceStatement extends DiagramStatement {
|
||||||
|
type: SequenceStatementType;
|
||||||
|
data: ParticipantData | MessageData | LoopData | NoteData | ActivateData | DeactivateData | AltData | OptData | ParData | RectData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete sequence diagram AST
|
||||||
|
*/
|
||||||
|
export interface SequenceAST extends DiagramAST {
|
||||||
|
header: 'sequenceDiagram';
|
||||||
|
statements: SequenceStatement[];
|
||||||
|
metadata?: {
|
||||||
|
title?: string;
|
||||||
|
participants?: Map<string, ParticipantData>;
|
||||||
|
theme?: string;
|
||||||
|
config?: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper functions for working with sequence AST
|
||||||
|
*/
|
||||||
|
export class SequenceASTHelper {
|
||||||
|
/**
|
||||||
|
* Get all participants from the AST
|
||||||
|
*/
|
||||||
|
static getParticipants(ast: SequenceAST): ParticipantData[] {
|
||||||
|
return ast.statements
|
||||||
|
.filter(stmt => stmt.type === 'participant')
|
||||||
|
.map(stmt => stmt.data as ParticipantData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all messages from the AST
|
||||||
|
*/
|
||||||
|
static getMessages(ast: SequenceAST): MessageData[] {
|
||||||
|
return ast.statements
|
||||||
|
.filter(stmt => stmt.type === 'message')
|
||||||
|
.map(stmt => stmt.data as MessageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all participants mentioned in messages (even if not explicitly declared)
|
||||||
|
*/
|
||||||
|
static getAllMentionedParticipants(ast: SequenceAST): Set<string> {
|
||||||
|
const participants = new Set<string>();
|
||||||
|
|
||||||
|
// Add explicitly declared participants
|
||||||
|
this.getParticipants(ast).forEach(p => participants.add(p.id));
|
||||||
|
|
||||||
|
// Add participants from messages
|
||||||
|
this.getMessages(ast).forEach(m => {
|
||||||
|
participants.add(m.from);
|
||||||
|
participants.add(m.to);
|
||||||
|
});
|
||||||
|
|
||||||
|
return participants;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find participant by ID
|
||||||
|
*/
|
||||||
|
static findParticipant(ast: SequenceAST, id: string): ParticipantData | undefined {
|
||||||
|
const stmt = ast.statements.find(stmt =>
|
||||||
|
stmt.type === 'participant' && (stmt.data as ParticipantData).id === id
|
||||||
|
);
|
||||||
|
return stmt ? stmt.data as ParticipantData : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display name for a participant (alias if available, otherwise ID)
|
||||||
|
*/
|
||||||
|
static getParticipantDisplayName(ast: SequenceAST, id: string): string {
|
||||||
|
const participant = this.findParticipant(ast, id);
|
||||||
|
return participant?.alias || participant?.displayName || id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a participant is explicitly declared
|
||||||
|
*/
|
||||||
|
static isParticipantDeclared(ast: SequenceAST, id: string): boolean {
|
||||||
|
return this.findParticipant(ast, id) !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the index of the first message involving a participant
|
||||||
|
*/
|
||||||
|
static getFirstMessageIndex(ast: SequenceAST, participantId: string): number {
|
||||||
|
return ast.statements.findIndex(stmt =>
|
||||||
|
stmt.type === 'message' &&
|
||||||
|
((stmt.data as MessageData).from === participantId || (stmt.data as MessageData).to === participantId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate AST structure
|
||||||
|
*/
|
||||||
|
static validate(ast: SequenceAST): { valid: boolean; errors: string[] } {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// Check for duplicate participant declarations
|
||||||
|
const participantIds = new Set<string>();
|
||||||
|
ast.statements
|
||||||
|
.filter(stmt => stmt.type === 'participant')
|
||||||
|
.forEach(stmt => {
|
||||||
|
const participant = stmt.data as ParticipantData;
|
||||||
|
if (participantIds.has(participant.id)) {
|
||||||
|
errors.push(`Duplicate participant declaration: ${participant.id}`);
|
||||||
|
}
|
||||||
|
participantIds.add(participant.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for messages with undefined participants
|
||||||
|
const allMentioned = this.getAllMentionedParticipants(ast);
|
||||||
|
this.getMessages(ast).forEach(message => {
|
||||||
|
if (!allMentioned.has(message.from)) {
|
||||||
|
errors.push(`Message references undefined participant: ${message.from}`);
|
||||||
|
}
|
||||||
|
if (!allMentioned.has(message.to)) {
|
||||||
|
errors.push(`Message references undefined participant: ${message.to}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for valid arrow types
|
||||||
|
const validArrows = ['->', '-->>', '->>', '-->', '-x', '--x', '-)', '--)', '<<->>', '<<-->>'];
|
||||||
|
this.getMessages(ast).forEach(message => {
|
||||||
|
if (!validArrows.includes(message.arrow)) {
|
||||||
|
errors.push(`Invalid arrow type: ${message.arrow}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: errors.length === 0,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statistics about the AST
|
||||||
|
*/
|
||||||
|
static getStatistics(ast: SequenceAST): {
|
||||||
|
totalStatements: number;
|
||||||
|
participants: number;
|
||||||
|
messages: number;
|
||||||
|
notes: number;
|
||||||
|
loops: number;
|
||||||
|
complexity: 'simple' | 'moderate' | 'complex';
|
||||||
|
} {
|
||||||
|
const stats = {
|
||||||
|
totalStatements: ast.statements.length,
|
||||||
|
participants: ast.statements.filter(s => s.type === 'participant').length,
|
||||||
|
messages: ast.statements.filter(s => s.type === 'message').length,
|
||||||
|
notes: ast.statements.filter(s => s.type === 'note').length,
|
||||||
|
loops: ast.statements.filter(s => s.type === 'loop').length,
|
||||||
|
complexity: 'simple' as 'simple' | 'moderate' | 'complex'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine complexity
|
||||||
|
if (stats.totalStatements > 50 || stats.loops > 3) {
|
||||||
|
stats.complexity = 'complex';
|
||||||
|
} else if (stats.totalStatements > 20 || stats.loops > 1) {
|
||||||
|
stats.complexity = 'moderate';
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a minimal valid sequence AST
|
||||||
|
*/
|
||||||
|
static createEmpty(): SequenceAST {
|
||||||
|
return {
|
||||||
|
header: 'sequenceDiagram',
|
||||||
|
statements: [],
|
||||||
|
metadata: {
|
||||||
|
participants: new Map()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone an AST (deep copy)
|
||||||
|
*/
|
||||||
|
static clone(ast: SequenceAST): SequenceAST {
|
||||||
|
return {
|
||||||
|
header: ast.header,
|
||||||
|
statements: ast.statements.map(stmt => ({
|
||||||
|
type: stmt.type,
|
||||||
|
originalIndex: stmt.originalIndex,
|
||||||
|
data: { ...stmt.data },
|
||||||
|
sourceTokens: stmt.sourceTokens
|
||||||
|
})),
|
||||||
|
metadata: ast.metadata ? {
|
||||||
|
title: ast.metadata.title,
|
||||||
|
participants: new Map(ast.metadata.participants),
|
||||||
|
theme: ast.metadata.theme,
|
||||||
|
config: ast.metadata.config ? { ...ast.metadata.config } : undefined
|
||||||
|
} : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,692 @@
|
|||||||
|
import type { SequenceParserVisitor } from './generated/SequenceParserVisitor.js';
|
||||||
|
import {
|
||||||
|
SequenceAST,
|
||||||
|
SequenceStatement,
|
||||||
|
ParticipantData,
|
||||||
|
MessageData,
|
||||||
|
NoteData,
|
||||||
|
SequenceASTHelper,
|
||||||
|
} from './SequenceAST.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AST-to-Code Generator for Sequence Diagrams
|
||||||
|
*
|
||||||
|
* This visitor traverses the ANTLR parse tree and reconstructs the original
|
||||||
|
* sequence diagram code with proper line numbers and formatting.
|
||||||
|
*
|
||||||
|
* Main objective: Enable UI editing of rendered diagrams with AST updates
|
||||||
|
* that can be regenerated back to code.
|
||||||
|
*
|
||||||
|
* Now also builds a structured AST for the hybrid editor approach.
|
||||||
|
*/
|
||||||
|
export class SequenceCodeGenerator implements SequenceParserVisitor<string> {
|
||||||
|
private lines: string[] = [];
|
||||||
|
private currentIndent = 0;
|
||||||
|
private indentSize = 2;
|
||||||
|
|
||||||
|
// AST building properties
|
||||||
|
private ast: SequenceAST;
|
||||||
|
private currentIndex = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Initialize with empty lines array
|
||||||
|
this.lines = [];
|
||||||
|
// Initialize AST
|
||||||
|
this.ast = SequenceASTHelper.createEmpty();
|
||||||
|
this.currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate code from the parse tree
|
||||||
|
*/
|
||||||
|
generateCode(tree: any): { code: string; lines: string[]; ast: SequenceAST } {
|
||||||
|
this.lines = [];
|
||||||
|
this.currentIndent = 0;
|
||||||
|
this.ast = SequenceASTHelper.createEmpty();
|
||||||
|
this.currentIndex = 0;
|
||||||
|
|
||||||
|
console.log('🎯 Starting code generation with AST building');
|
||||||
|
|
||||||
|
// Visit the tree to generate code and build AST
|
||||||
|
this.visit(tree);
|
||||||
|
|
||||||
|
// Join lines and return both full code, line array, and AST
|
||||||
|
const code = this.lines.join('\n');
|
||||||
|
|
||||||
|
console.log('✅ Code generation complete:', {
|
||||||
|
lines: this.lines.length,
|
||||||
|
statements: this.ast.statements.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
lines: [...this.lines], // Return copy of lines array
|
||||||
|
ast: this.ast, // Return the built AST
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current AST (for external access)
|
||||||
|
*/
|
||||||
|
getAST(): SequenceAST {
|
||||||
|
return this.ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a line with current indentation
|
||||||
|
*/
|
||||||
|
private addLine(text: string): void {
|
||||||
|
const indent = ' '.repeat(this.currentIndent);
|
||||||
|
this.lines.push(indent + text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a line without indentation
|
||||||
|
*/
|
||||||
|
private addRawLine(text: string): void {
|
||||||
|
this.lines.push(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increase indentation level
|
||||||
|
*/
|
||||||
|
private indent(): void {
|
||||||
|
this.currentIndent += this.indentSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrease indentation level
|
||||||
|
*/
|
||||||
|
private unindent(): void {
|
||||||
|
this.currentIndent = Math.max(0, this.currentIndent - this.indentSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract text from terminal nodes
|
||||||
|
*/
|
||||||
|
private getTerminalText(ctx: any): string {
|
||||||
|
if (!ctx) return '';
|
||||||
|
|
||||||
|
// If it's a terminal node, return its text
|
||||||
|
if (ctx.symbol?.text) {
|
||||||
|
return ctx.symbol.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it has children, collect text from all terminal children
|
||||||
|
if (ctx.children) {
|
||||||
|
return ctx.children
|
||||||
|
.map((child: any) => this.getTerminalText(child))
|
||||||
|
.filter((text: string) => text.trim() !== '')
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get text content from a context, handling both terminal and non-terminal nodes
|
||||||
|
*/
|
||||||
|
private getContextText(ctx: any): string {
|
||||||
|
if (!ctx) return '';
|
||||||
|
|
||||||
|
// Use ANTLR's built-in getText() method which is most reliable
|
||||||
|
if (ctx.getText) {
|
||||||
|
return ctx.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getTerminalText(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple approach: extract all text from the parse tree and reconstruct line by line
|
||||||
|
* This is more reliable than trying to handle each rule type individually
|
||||||
|
*/
|
||||||
|
private extractAllText(ctx: any): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
if (!ctx) return lines;
|
||||||
|
|
||||||
|
// Get the full text content
|
||||||
|
const fullText = ctx.getText ? ctx.getText() : '';
|
||||||
|
|
||||||
|
if (fullText) {
|
||||||
|
// Split by common sequence diagram patterns and clean up
|
||||||
|
const rawLines = fullText.split(/\n+/);
|
||||||
|
|
||||||
|
for (const line of rawLines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed && trimmed !== 'sequenceDiagram') {
|
||||||
|
lines.push(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default visit method
|
||||||
|
visit(tree: any): string {
|
||||||
|
if (!tree) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
return tree.accept(this) || '';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error visiting node:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default visit methods
|
||||||
|
visitChildren(node: any): string {
|
||||||
|
if (!node || !node.children) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: string[] = [];
|
||||||
|
for (const child of node.children) {
|
||||||
|
const result = child.accept(this);
|
||||||
|
if (result) {
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
visitTerminal(node: any): string {
|
||||||
|
return node.symbol?.text || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
visitErrorNode(_node: any): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start rule - the root of the parse tree
|
||||||
|
visitStart(ctx: any): string {
|
||||||
|
// Proper visitor approach: use the AST structure
|
||||||
|
console.log('🎯 visitStart: Starting AST traversal');
|
||||||
|
|
||||||
|
// Add the header
|
||||||
|
this.addRawLine('sequenceDiagram');
|
||||||
|
|
||||||
|
// Visit header first (if any)
|
||||||
|
if (ctx.header?.()) {
|
||||||
|
this.visit(ctx.header());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit document content
|
||||||
|
if (ctx.document?.()) {
|
||||||
|
this.visit(ctx.document());
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📋 Final generated lines:', this.lines);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header - handle front matter, comments, etc.
|
||||||
|
visitHeader(ctx: any): string {
|
||||||
|
// Process header directives, front matter, etc.
|
||||||
|
if (ctx.children) {
|
||||||
|
for (const child of ctx.children) {
|
||||||
|
const text = this.getContextText(child);
|
||||||
|
if (text && text.trim() !== '' && text !== '\n') {
|
||||||
|
this.addRawLine(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Document - main content
|
||||||
|
visitDocument(ctx: any): string {
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line - individual lines in the document
|
||||||
|
visitLine(ctx: any): string {
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statement - individual statements
|
||||||
|
visitStatement(ctx: any): string {
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Participant statement
|
||||||
|
visitParticipantStatement(ctx: any): string {
|
||||||
|
console.log('🎯 visitParticipantStatement:', ctx);
|
||||||
|
|
||||||
|
// Use the simpler approach: get the full text and clean it up
|
||||||
|
const fullText = ctx.getText ? ctx.getText() : '';
|
||||||
|
console.log(' - Full participant text:', fullText);
|
||||||
|
|
||||||
|
if (fullText) {
|
||||||
|
let id = '';
|
||||||
|
let alias = '';
|
||||||
|
|
||||||
|
// Parse the participant pattern: participant + id + as + alias
|
||||||
|
const participantMatch = fullText.match(/^participant(\w+)as(.+)$/);
|
||||||
|
if (participantMatch) {
|
||||||
|
[, id, alias] = participantMatch;
|
||||||
|
alias = alias.trim();
|
||||||
|
this.addLine(`participant ${id} as ${alias}`);
|
||||||
|
} else {
|
||||||
|
// Try simple participant without alias
|
||||||
|
const simpleMatch = fullText.match(/^participant(\w+)$/);
|
||||||
|
if (simpleMatch) {
|
||||||
|
[, id] = simpleMatch;
|
||||||
|
this.addLine(`participant ${id}`);
|
||||||
|
} else {
|
||||||
|
// Fallback: just use the text as-is with proper indentation
|
||||||
|
this.addLine(fullText);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build AST entry
|
||||||
|
const participantData: ParticipantData = { id, alias: alias || undefined };
|
||||||
|
this.ast.statements.push({
|
||||||
|
type: 'participant',
|
||||||
|
originalIndex: this.currentIndex++,
|
||||||
|
data: participantData,
|
||||||
|
sourceTokens: { start: ctx.start, stop: ctx.stop },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📝 Added participant to AST:', participantData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create statement
|
||||||
|
visitCreateStatement(ctx: any): string {
|
||||||
|
console.log('🎯 visitCreateStatement:', ctx);
|
||||||
|
const text = this.getContextText(ctx);
|
||||||
|
this.addLine(text);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy statement
|
||||||
|
visitDestroyStatement(ctx: any): string {
|
||||||
|
console.log('🎯 visitDestroyStatement:', ctx);
|
||||||
|
const text = this.getContextText(ctx);
|
||||||
|
this.addLine(text);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal statement (messages between participants)
|
||||||
|
visitSignalStatement(ctx: any): string {
|
||||||
|
console.log('🎯 visitSignalStatement:', ctx);
|
||||||
|
|
||||||
|
// Use the simpler approach: get the full text and clean it up
|
||||||
|
const fullText = ctx.getText ? ctx.getText() : '';
|
||||||
|
console.log(' - Full signal text:', fullText);
|
||||||
|
|
||||||
|
if (fullText) {
|
||||||
|
// Parse the signal pattern: from + arrow + to + : + message
|
||||||
|
const signalMatch = fullText.match(/^(\w+)(->|-->>|->>|-->)(\w+):(.+)$/);
|
||||||
|
if (signalMatch) {
|
||||||
|
const [, from, arrow, to, message] = signalMatch;
|
||||||
|
const cleanMessage = message.trim();
|
||||||
|
this.addLine(`${from}${arrow}${to}: ${cleanMessage}`);
|
||||||
|
|
||||||
|
// Build AST entry
|
||||||
|
const messageData: MessageData = { from, arrow, to, message: cleanMessage };
|
||||||
|
this.ast.statements.push({
|
||||||
|
type: 'message',
|
||||||
|
originalIndex: this.currentIndex++,
|
||||||
|
data: messageData,
|
||||||
|
sourceTokens: { start: ctx.start, stop: ctx.stop },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📝 Added message to AST:', messageData);
|
||||||
|
} else {
|
||||||
|
// Fallback: just use the text as-is with proper indentation
|
||||||
|
this.addLine(fullText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note statement
|
||||||
|
visitNoteStatement(ctx: any): string {
|
||||||
|
console.log('🎯 visitNoteStatement:', ctx);
|
||||||
|
|
||||||
|
// Use the simpler approach: get the full text and clean it up
|
||||||
|
const fullText = ctx.getText ? ctx.getText() : '';
|
||||||
|
console.log(' - Full note text:', fullText);
|
||||||
|
|
||||||
|
if (fullText) {
|
||||||
|
// Parse the note pattern: Note + position + of + participant + : + message
|
||||||
|
const noteMatch = fullText.match(/^Note(left|right|over)of(\w+):(.+)$/);
|
||||||
|
if (noteMatch) {
|
||||||
|
const [, position, participant, message] = noteMatch;
|
||||||
|
this.addLine(`Note ${position} of ${participant}: ${message.trim()}`);
|
||||||
|
} else {
|
||||||
|
// Fallback: just use the text as-is with proper indentation
|
||||||
|
this.addLine(fullText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop block
|
||||||
|
visitLoopBlock(ctx: any): string {
|
||||||
|
console.log('🎯 visitLoopBlock:', ctx);
|
||||||
|
|
||||||
|
// Use the simpler approach: get the full text and extract loop condition
|
||||||
|
const fullText = ctx.getText ? ctx.getText() : '';
|
||||||
|
console.log(' - Full loop text:', fullText);
|
||||||
|
|
||||||
|
if (fullText) {
|
||||||
|
// Extract the loop condition - everything between "loop" and the first statement
|
||||||
|
const loopMatch = fullText.match(/^loop([^]*?)(?=\w+(?:->|-->>|->>|-->)|$)/);
|
||||||
|
if (loopMatch) {
|
||||||
|
const condition = loopMatch[1].trim();
|
||||||
|
this.addLine(`loop ${condition}`);
|
||||||
|
} else {
|
||||||
|
this.addLine('loop');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.indent();
|
||||||
|
|
||||||
|
// Visit children (content inside loop)
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
|
||||||
|
this.unindent();
|
||||||
|
this.addLine('end');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opt block
|
||||||
|
visitOptBlock(ctx: any): string {
|
||||||
|
const optText = this.getContextText(ctx);
|
||||||
|
const optMatch = optText.match(/opt\s+(.+?)(?=\s|$)/);
|
||||||
|
const condition = optMatch ? optMatch[1] : '';
|
||||||
|
|
||||||
|
this.addLine(`opt ${condition}`);
|
||||||
|
this.indent();
|
||||||
|
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
|
||||||
|
this.unindent();
|
||||||
|
this.addLine('end');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alt block
|
||||||
|
visitAltBlock(ctx: any): string {
|
||||||
|
const altText = this.getContextText(ctx);
|
||||||
|
const altMatch = altText.match(/alt\s+(.+?)(?=\s|$)/);
|
||||||
|
const condition = altMatch ? altMatch[1] : '';
|
||||||
|
|
||||||
|
this.addLine(`alt ${condition}`);
|
||||||
|
this.indent();
|
||||||
|
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
|
||||||
|
this.unindent();
|
||||||
|
this.addLine('end');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else section within alt block
|
||||||
|
visitElseSection(ctx: any): string {
|
||||||
|
this.unindent();
|
||||||
|
|
||||||
|
const elseText = this.getContextText(ctx);
|
||||||
|
const elseMatch = elseText.match(/else\s+(.+?)(?=\s|$)/);
|
||||||
|
const condition = elseMatch ? elseMatch[1] : '';
|
||||||
|
|
||||||
|
this.addLine(`else ${condition}`);
|
||||||
|
this.indent();
|
||||||
|
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Par block
|
||||||
|
visitParBlock(ctx: any): string {
|
||||||
|
const parText = this.getContextText(ctx);
|
||||||
|
const parMatch = parText.match(/par\s+(.+?)(?=\s|$)/);
|
||||||
|
const condition = parMatch ? parMatch[1] : '';
|
||||||
|
|
||||||
|
this.addLine(`par ${condition}`);
|
||||||
|
this.indent();
|
||||||
|
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
|
||||||
|
this.unindent();
|
||||||
|
this.addLine('end');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// And section within par block
|
||||||
|
visitAndSection(ctx: any): string {
|
||||||
|
this.unindent();
|
||||||
|
|
||||||
|
const andText = this.getContextText(ctx);
|
||||||
|
const andMatch = andText.match(/and\s+(.+?)(?=\s|$)/);
|
||||||
|
const condition = andMatch ? andMatch[1] : '';
|
||||||
|
|
||||||
|
this.addLine(`and ${condition}`);
|
||||||
|
this.indent();
|
||||||
|
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rect block
|
||||||
|
visitRectBlock(ctx: any): string {
|
||||||
|
const rectText = this.getContextText(ctx);
|
||||||
|
const rectMatch = rectText.match(/rect\s+(.+?)(?=\s|$)/);
|
||||||
|
const style = rectMatch ? rectMatch[1] : '';
|
||||||
|
|
||||||
|
this.addLine(`rect ${style}`);
|
||||||
|
this.indent();
|
||||||
|
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
|
||||||
|
this.unindent();
|
||||||
|
this.addLine('end');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box block
|
||||||
|
visitBoxBlock(ctx: any): string {
|
||||||
|
const boxText = this.getContextText(ctx);
|
||||||
|
const boxMatch = boxText.match(/box\s+(.+?)(?=\s|$)/);
|
||||||
|
const label = boxMatch ? boxMatch[1] : '';
|
||||||
|
|
||||||
|
this.addLine(`box ${label}`);
|
||||||
|
this.indent();
|
||||||
|
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
|
||||||
|
this.unindent();
|
||||||
|
this.addLine('end');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Break block
|
||||||
|
visitBreakBlock(ctx: any): string {
|
||||||
|
const breakText = this.getContextText(ctx);
|
||||||
|
const breakMatch = breakText.match(/break\s+(.+?)(?=\s|$)/);
|
||||||
|
const condition = breakMatch ? breakMatch[1] : '';
|
||||||
|
|
||||||
|
this.addLine(`break ${condition}`);
|
||||||
|
this.indent();
|
||||||
|
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
|
||||||
|
this.unindent();
|
||||||
|
this.addLine('end');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Critical block
|
||||||
|
visitCriticalBlock(ctx: any): string {
|
||||||
|
const criticalText = this.getContextText(ctx);
|
||||||
|
const criticalMatch = criticalText.match(/critical\s+(.+?)(?=\s|$)/);
|
||||||
|
const condition = criticalMatch ? criticalMatch[1] : '';
|
||||||
|
|
||||||
|
this.addLine(`critical ${condition}`);
|
||||||
|
this.indent();
|
||||||
|
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
|
||||||
|
this.unindent();
|
||||||
|
this.addLine('end');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option section within critical block
|
||||||
|
visitOptionSection(ctx: any): string {
|
||||||
|
this.unindent();
|
||||||
|
|
||||||
|
const optionText = this.getContextText(ctx);
|
||||||
|
const optionMatch = optionText.match(/option\s+(.+?)(?=\s|$)/);
|
||||||
|
const condition = optionMatch ? optionMatch[1] : '';
|
||||||
|
|
||||||
|
this.addLine(`option ${condition}`);
|
||||||
|
this.indent();
|
||||||
|
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParOver block
|
||||||
|
visitParOverBlock(ctx: any): string {
|
||||||
|
const parOverText = this.getContextText(ctx);
|
||||||
|
const parOverMatch = parOverText.match(/par\s+over\s+(.+?)(?=\s|$)/);
|
||||||
|
const participants = parOverMatch ? parOverMatch[1] : '';
|
||||||
|
|
||||||
|
this.addLine(`par over ${participants}`);
|
||||||
|
this.indent();
|
||||||
|
|
||||||
|
this.visitChildren(ctx);
|
||||||
|
|
||||||
|
this.unindent();
|
||||||
|
this.addLine('end');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links statement
|
||||||
|
visitLinksStatement(ctx: any): string {
|
||||||
|
const text = this.getContextText(ctx);
|
||||||
|
this.addLine(text);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link statement
|
||||||
|
visitLinkStatement(ctx: any): string {
|
||||||
|
const text = this.getContextText(ctx);
|
||||||
|
this.addLine(text);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties statement
|
||||||
|
visitPropertiesStatement(ctx: any): string {
|
||||||
|
const text = this.getContextText(ctx);
|
||||||
|
this.addLine(text);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Details statement
|
||||||
|
visitDetailsStatement(ctx: any): string {
|
||||||
|
const text = this.getContextText(ctx);
|
||||||
|
this.addLine(text);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activation statement (activate/deactivate)
|
||||||
|
visitActivationStatement(ctx: any): string {
|
||||||
|
const text = this.getContextText(ctx);
|
||||||
|
this.addLine(text);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Autonumber statement
|
||||||
|
visitAutonumberStatement(ctx: any): string {
|
||||||
|
const text = this.getContextText(ctx);
|
||||||
|
this.addLine(text);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title statement
|
||||||
|
visitTitleStatement(ctx: any): string {
|
||||||
|
const text = this.getContextText(ctx);
|
||||||
|
this.addLine(text);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy title statement
|
||||||
|
visitLegacyTitleStatement(ctx: any): string {
|
||||||
|
const text = this.getContextText(ctx);
|
||||||
|
this.addLine(text);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessibility title statement
|
||||||
|
visitAccTitleStatement(ctx: any): string {
|
||||||
|
const text = this.getContextText(ctx);
|
||||||
|
this.addLine(text);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessibility description statement
|
||||||
|
visitAccDescrStatement(ctx: any): string {
|
||||||
|
const text = this.getContextText(ctx);
|
||||||
|
this.addLine(text);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessibility multiline description statement
|
||||||
|
visitAccDescrMultilineStatement(ctx: any): string {
|
||||||
|
const text = this.getContextText(ctx);
|
||||||
|
this.addLine(text);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional visitor methods for completeness
|
||||||
|
visitActorWithConfig(ctx: any): string {
|
||||||
|
return this.visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitConfigObject(ctx: any): string {
|
||||||
|
return this.visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitSignaltype(ctx: any): string {
|
||||||
|
return this.visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitText2(ctx: any): string {
|
||||||
|
return this.visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitRestOfLine(ctx: any): string {
|
||||||
|
return this.visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitAltSections(ctx: any): string {
|
||||||
|
return this.visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitParSections(ctx: any): string {
|
||||||
|
return this.visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitOptionSections(ctx: any): string {
|
||||||
|
return this.visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitActor(ctx: any): string {
|
||||||
|
return this.visitChildren(ctx);
|
||||||
|
}
|
||||||
|
}
|
@@ -12,6 +12,8 @@ import { SequenceLexer } from './generated/SequenceLexer.js';
|
|||||||
import { SequenceParser } from './generated/SequenceParser.js';
|
import { SequenceParser } from './generated/SequenceParser.js';
|
||||||
import { SequenceListener } from './SequenceListener.js';
|
import { SequenceListener } from './SequenceListener.js';
|
||||||
import { SequenceVisitor } from './SequenceVisitor.js';
|
import { SequenceVisitor } from './SequenceVisitor.js';
|
||||||
|
import { SequenceCodeGenerator } from './SequenceCodeGenerator.js';
|
||||||
|
import { TokenStreamRewriter } from 'antlr4ng';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main ANTLR parser class that provides the same interface as the Jison parser
|
* Main ANTLR parser class that provides the same interface as the Jison parser
|
||||||
@@ -190,6 +192,49 @@ export class ANTLRSequenceParser {
|
|||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('✅ ANTLR Sequence Parser: Parse completed successfully');
|
console.log('✅ ANTLR Sequence Parser: Parse completed successfully');
|
||||||
}
|
}
|
||||||
|
// Store the parse tree for AST-to-code regeneration
|
||||||
|
this.yy._parseTree = tree;
|
||||||
|
|
||||||
|
// Build and store the AST during initial parsing
|
||||||
|
const astBuildStart = performance.now();
|
||||||
|
if (shouldLog) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('🌳 ANTLR Sequence Parser: Building AST from parse tree');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Store the original input and token stream for formatting preservation
|
||||||
|
this.yy._originalInput = input;
|
||||||
|
this.yy._tokenStream = tokenStream;
|
||||||
|
this.yy._tokenRewriter = new TokenStreamRewriter(tokenStream);
|
||||||
|
this.yy._tokenMap = new Map(); // Map statement indices to token positions
|
||||||
|
|
||||||
|
const generator = new SequenceCodeGenerator();
|
||||||
|
const result = generator.generateCode(tree);
|
||||||
|
|
||||||
|
// Store the AST in the parser state
|
||||||
|
this.yy._ast = result.ast;
|
||||||
|
this.yy._generatedCode = result.code;
|
||||||
|
this.yy._generatedLines = result.lines;
|
||||||
|
|
||||||
|
// Store original AST for change detection (only during initial parsing)
|
||||||
|
this.yy._originalAST = this.createSafeASTCopy(result.ast);
|
||||||
|
|
||||||
|
const astBuildTime = performance.now() - astBuildStart;
|
||||||
|
if (shouldLog) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`⏱️ AST building took: ${astBuildTime.toFixed(2)}ms`);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`🌳 AST built with ${result.ast.statements.length} statements`);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('✅ ANTLR Sequence Parser: AST built successfully');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('❌ ANTLR Sequence Parser: AST building failed:', error.message);
|
||||||
|
// Don't throw - AST building is optional, parsing should still succeed
|
||||||
|
}
|
||||||
|
|
||||||
return this.yy;
|
return this.yy;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const totalTime = performance.now() - startTime;
|
const totalTime = performance.now() - startTime;
|
||||||
@@ -205,8 +250,485 @@ export class ANTLRSequenceParser {
|
|||||||
setYY(yy: any) {
|
setYY(yy: any) {
|
||||||
this.yy = yy;
|
this.yy = yy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the AST that was built during parsing
|
||||||
|
* This provides immediate access to structured data without regeneration
|
||||||
|
*/
|
||||||
|
getAST(): any | null {
|
||||||
|
return this.yy._ast || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the generated code that was built during parsing
|
||||||
|
*/
|
||||||
|
getGeneratedCode(): string | null {
|
||||||
|
return this.yy._generatedCode || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the generated lines that were built during parsing
|
||||||
|
*/
|
||||||
|
getGeneratedLines(): string[] | null {
|
||||||
|
return this.yy._generatedLines || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate code preserving original formatting using smart formatting preservation
|
||||||
|
* This is the "hybrid" approach that maintains whitespace and indentation
|
||||||
|
*/
|
||||||
|
regenerateCodeWithFormatting(): string | null {
|
||||||
|
if (!this.yy._originalInput) {
|
||||||
|
console.warn('⚠️ Original input not available - falling back to AST regeneration');
|
||||||
|
const astResult = this.generateCodeFromAST();
|
||||||
|
return astResult ? astResult.code : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if AST has been modified since initial parsing
|
||||||
|
const currentAST = this.yy._ast;
|
||||||
|
const hasASTChanges = this.detectASTChanges(currentAST);
|
||||||
|
|
||||||
|
if (hasASTChanges) {
|
||||||
|
console.log(
|
||||||
|
'🔄 AST changes detected, applying selective updates with formatting preservation'
|
||||||
|
);
|
||||||
|
return this.applyASTChangesWithFormatting(currentAST);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
'✅ No AST changes detected, returning original input with preserved formatting'
|
||||||
|
);
|
||||||
|
return this.yy._originalInput;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error regenerating code with formatting:', error);
|
||||||
|
// Fallback to AST regeneration
|
||||||
|
const astResult = this.generateCodeFromAST();
|
||||||
|
return astResult ? astResult.code : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if the AST has been modified since initial parsing
|
||||||
|
* Uses safe comparison that avoids circular reference issues
|
||||||
|
*/
|
||||||
|
private detectASTChanges(currentAST: any): boolean {
|
||||||
|
if (!this.yy._originalAST) {
|
||||||
|
console.warn('⚠️ No original AST stored for comparison - assuming changes detected');
|
||||||
|
return true; // If no original AST, assume changes to trigger regeneration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare safe representations to detect changes
|
||||||
|
const originalSafe = this.createSafeASTCopy(this.yy._originalAST);
|
||||||
|
const currentSafe = this.createSafeASTCopy(currentAST);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const originalJSON = JSON.stringify(originalSafe);
|
||||||
|
const currentJSON = JSON.stringify(currentSafe);
|
||||||
|
return originalJSON !== currentJSON;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ AST comparison failed, assuming changes detected:', error);
|
||||||
|
return true; // Assume changes if comparison fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a safe copy of AST that can be JSON.stringify'd
|
||||||
|
* Removes circular references and focuses on the data we care about
|
||||||
|
*/
|
||||||
|
private createSafeASTCopy(ast: any): any {
|
||||||
|
if (!ast || !ast.statements) {
|
||||||
|
return { statements: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
statements: ast.statements.map((stmt: any) => ({
|
||||||
|
type: stmt.type,
|
||||||
|
originalIndex: stmt.originalIndex,
|
||||||
|
// Handle both direct properties and data object structure
|
||||||
|
data: stmt.data
|
||||||
|
? {
|
||||||
|
from: stmt.data.from,
|
||||||
|
to: stmt.data.to,
|
||||||
|
message: stmt.data.message,
|
||||||
|
arrow: stmt.data.arrow,
|
||||||
|
participant: stmt.data.participant,
|
||||||
|
id: stmt.data.id,
|
||||||
|
alias: stmt.data.alias,
|
||||||
|
position: stmt.data.position,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
// Legacy direct properties (fallback)
|
||||||
|
from: stmt.from,
|
||||||
|
to: stmt.to,
|
||||||
|
message: stmt.message,
|
||||||
|
arrow: stmt.arrow,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply AST changes while preserving original formatting using TokenStreamRewriter
|
||||||
|
* This is where the real hybrid magic happens!
|
||||||
|
*/
|
||||||
|
private applyASTChangesWithFormatting(currentAST: any): string {
|
||||||
|
if (!this.yy._tokenRewriter || !this.yy._originalAST) {
|
||||||
|
console.log('🚧 TokenStreamRewriter not available, falling back to AST regeneration');
|
||||||
|
const astResult = this.generateCodeFromModifiedAST(currentAST);
|
||||||
|
return astResult ? astResult.code : this.yy._originalInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🎯 Using TokenStreamRewriter for surgical edits...');
|
||||||
|
|
||||||
|
// Find specific changes between original and current AST
|
||||||
|
const changes = this.detectSpecificChanges(this.yy._originalAST, currentAST);
|
||||||
|
|
||||||
|
if (changes.length === 0) {
|
||||||
|
console.log('✅ No specific changes detected, returning original input');
|
||||||
|
return this.yy._originalInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔧 Applying ${changes.length} surgical change(s)...`);
|
||||||
|
|
||||||
|
// Apply each change using surgical string replacement
|
||||||
|
changes.forEach((change, index) => {
|
||||||
|
if (change.type === 'add') {
|
||||||
|
console.log(
|
||||||
|
` ${index + 1}. ${change.type}: Added "${change.statement?.type}" statement`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
` ${index + 1}. ${change.type}: "${change.oldValue}" → "${change.newValue}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.applyTokenChange(change);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the surgically modified original input
|
||||||
|
console.log('✅ Surgical edits applied successfully');
|
||||||
|
console.log('📝 Modified text:', this.yy._originalInput);
|
||||||
|
|
||||||
|
return this.yy._originalInput;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ TokenStreamRewriter failed, falling back to AST regeneration:', error);
|
||||||
|
const astResult = this.generateCodeFromModifiedAST(currentAST);
|
||||||
|
return astResult ? astResult.code : this.yy._originalInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect specific changes between original and current AST
|
||||||
|
* Returns an array of change objects that can be applied surgically
|
||||||
|
*/
|
||||||
|
private detectSpecificChanges(originalAST: any, currentAST: any): any[] {
|
||||||
|
const changes: any[] = [];
|
||||||
|
|
||||||
|
if (!originalAST?.statements || !currentAST?.statements) {
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare statements by originalIndex to detect changes
|
||||||
|
const originalMap = new Map();
|
||||||
|
originalAST.statements.forEach((stmt: any) => {
|
||||||
|
originalMap.set(stmt.originalIndex, stmt);
|
||||||
|
});
|
||||||
|
|
||||||
|
currentAST.statements.forEach((currentStmt: any) => {
|
||||||
|
const originalStmt = originalMap.get(currentStmt.originalIndex);
|
||||||
|
if (!originalStmt) {
|
||||||
|
// New statement added
|
||||||
|
changes.push({
|
||||||
|
type: 'add',
|
||||||
|
statementIndex: currentStmt.originalIndex,
|
||||||
|
statement: currentStmt,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for message changes in existing statements
|
||||||
|
if (currentStmt.type === 'message' && originalStmt.type === 'message') {
|
||||||
|
const currentData = currentStmt.data || currentStmt;
|
||||||
|
const originalData = originalStmt.data || originalStmt;
|
||||||
|
|
||||||
|
if (currentData.message !== originalData.message) {
|
||||||
|
changes.push({
|
||||||
|
type: 'message_change',
|
||||||
|
statementIndex: currentStmt.originalIndex,
|
||||||
|
oldValue: originalData.message,
|
||||||
|
newValue: currentData.message,
|
||||||
|
statement: currentStmt,
|
||||||
|
originalStatement: originalStmt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a specific change using string-based replacement
|
||||||
|
* This performs surgical edits preserving original formatting
|
||||||
|
*/
|
||||||
|
private applyTokenChange(change: any): void {
|
||||||
|
if (!this.yy._originalInput) {
|
||||||
|
throw new Error('Original input not available for surgical edits');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (change.type === 'message_change') {
|
||||||
|
// Find the message location in the original input
|
||||||
|
const messageTokens = this.findMessageTokensForStatement(change.statementIndex);
|
||||||
|
|
||||||
|
if (messageTokens && messageTokens.length > 0) {
|
||||||
|
const token = messageTokens[0];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`🔧 Replacing message at line ${token.lineIndex}, pos ${token.messageStart}-${token.messageEnd}: "${token.originalText}" → "${change.newValue}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Perform string-based replacement preserving formatting
|
||||||
|
const lines = this.yy._originalInput.split('\n');
|
||||||
|
const targetLine = lines[token.lineIndex];
|
||||||
|
|
||||||
|
// Replace only the message part, preserving everything else
|
||||||
|
const newLine =
|
||||||
|
targetLine.substring(0, token.messageStart) +
|
||||||
|
change.newValue +
|
||||||
|
targetLine.substring(token.messageEnd);
|
||||||
|
|
||||||
|
lines[token.lineIndex] = newLine;
|
||||||
|
|
||||||
|
// Update the original input with the surgical change
|
||||||
|
this.yy._originalInput = lines.join('\n');
|
||||||
|
|
||||||
|
console.log(`✅ Surgical edit applied successfully`);
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ Could not find message tokens for statement ${change.statementIndex}`);
|
||||||
|
throw new Error('Message tokens not found');
|
||||||
|
}
|
||||||
|
} else if (change.type === 'add') {
|
||||||
|
// For additions (like participants), surgical editing is complex
|
||||||
|
// Fall back to full AST regeneration for now
|
||||||
|
console.log(`🔄 Addition detected, falling back to full AST regeneration`);
|
||||||
|
throw new Error('Addition requires full AST regeneration');
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ Unsupported change type: ${change.type}`);
|
||||||
|
throw new Error(`Unsupported change type: ${change.type}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error applying surgical change:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the message tokens for a specific statement in the parse tree
|
||||||
|
* This maps AST statements back to their original tokens
|
||||||
|
*/
|
||||||
|
private findMessageTokensForStatement(statementIndex: number): any[] | null {
|
||||||
|
// For now, use a simpler approach: find the message text in the original input
|
||||||
|
// and create pseudo-tokens for replacement
|
||||||
|
|
||||||
|
if (!this.yy._originalInput || !this.yy._originalAST) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the original statement
|
||||||
|
const originalStmt = this.yy._originalAST.statements.find(
|
||||||
|
(stmt: any) => stmt.originalIndex === statementIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!originalStmt || originalStmt.type !== 'message') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalData = originalStmt.data || originalStmt;
|
||||||
|
const originalMessage = originalData.message;
|
||||||
|
|
||||||
|
if (!originalMessage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the message text in the original input
|
||||||
|
const lines = this.yy._originalInput.split('\n');
|
||||||
|
let lineIndex = -1;
|
||||||
|
let messageStart = -1;
|
||||||
|
let messageEnd = -1;
|
||||||
|
|
||||||
|
// Look for the message in each line
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const messageIndex = line.indexOf(originalMessage);
|
||||||
|
|
||||||
|
if (messageIndex !== -1) {
|
||||||
|
// Check if this looks like a sequence diagram message line
|
||||||
|
const beforeMessage = line.substring(0, messageIndex);
|
||||||
|
if (beforeMessage.includes('>>') || beforeMessage.includes('->')) {
|
||||||
|
lineIndex = i;
|
||||||
|
messageStart = messageIndex;
|
||||||
|
messageEnd = messageIndex + originalMessage.length;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineIndex === -1) {
|
||||||
|
console.log(`🔍 Could not find message "${originalMessage}" in original input`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create pseudo-tokens for the message text
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
lineIndex,
|
||||||
|
messageStart,
|
||||||
|
messageEnd,
|
||||||
|
originalText: originalMessage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error finding message tokens:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate code directly from a modified AST structure
|
||||||
|
* This bypasses the parse tree and works directly with AST data
|
||||||
|
*/
|
||||||
|
private generateCodeFromModifiedAST(
|
||||||
|
ast: any
|
||||||
|
): { code: string; lines: string[]; ast: any } | null {
|
||||||
|
if (!ast || !ast.statements) {
|
||||||
|
console.warn('⚠️ No AST statements available for code generation');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate code directly from AST statements
|
||||||
|
const lines: string[] = ['sequenceDiagram'];
|
||||||
|
|
||||||
|
// Sort statements by originalIndex to maintain proper order
|
||||||
|
const sortedStatements = [...ast.statements].sort((a, b) => {
|
||||||
|
const aIndex = a.originalIndex ?? 999;
|
||||||
|
const bIndex = b.originalIndex ?? 999;
|
||||||
|
return aIndex - bIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🔍 Debug: Processing statements for code generation:');
|
||||||
|
console.log('📊 Total statements:', ast.statements.length);
|
||||||
|
sortedStatements.forEach((stmt, index) => {
|
||||||
|
console.log(
|
||||||
|
` ${index + 1}. Type: ${stmt.type}, OriginalIndex: ${stmt.originalIndex}, Data:`,
|
||||||
|
stmt.data || 'N/A'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process each statement in the AST
|
||||||
|
sortedStatements.forEach((stmt: any) => {
|
||||||
|
if (stmt.type === 'message') {
|
||||||
|
const data = stmt.data || stmt; // Handle both data object and direct properties
|
||||||
|
const from = data.from || '';
|
||||||
|
const to = data.to || '';
|
||||||
|
const message = data.message || '';
|
||||||
|
const arrow = data.arrow || '->>';
|
||||||
|
|
||||||
|
// Generate the message line
|
||||||
|
const messageLine = `${from}${arrow}${to}: ${message}`;
|
||||||
|
lines.push(messageLine);
|
||||||
|
} else if (stmt.type === 'participant') {
|
||||||
|
const data = stmt.data || stmt;
|
||||||
|
const id = data.id || data.participant || '';
|
||||||
|
const alias = data.alias || '';
|
||||||
|
|
||||||
|
// Generate participant line with optional alias
|
||||||
|
if (alias) {
|
||||||
|
lines.push(`participant ${id} as "${alias}"`);
|
||||||
|
} else {
|
||||||
|
lines.push(`participant ${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add more statement types as needed
|
||||||
|
});
|
||||||
|
|
||||||
|
const code = lines.join('\n');
|
||||||
|
|
||||||
|
console.log('✅ Code generated from modified AST:');
|
||||||
|
console.log('📝 Generated code:', code);
|
||||||
|
console.log('📋 Generated lines:', lines);
|
||||||
|
console.log('🌳 AST statements:', ast.statements.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
lines,
|
||||||
|
ast,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error generating code from modified AST:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate code from the stored parse tree
|
||||||
|
* This enables AST-to-code regeneration for UI editing scenarios
|
||||||
|
* Now also returns the structured AST for hybrid editing
|
||||||
|
*/
|
||||||
|
generateCodeFromAST(): { code: string; lines: string[]; ast: any } | null {
|
||||||
|
if (!this.yy._parseTree) {
|
||||||
|
console.warn('⚠️ No parse tree available for code generation');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const generator = new SequenceCodeGenerator();
|
||||||
|
const result = generator.generateCode(this.yy._parseTree);
|
||||||
|
|
||||||
|
console.log('✅ Code generated from AST:');
|
||||||
|
console.log('📝 Generated code:', result.code);
|
||||||
|
console.log('📋 Generated lines:', result.lines);
|
||||||
|
console.log('🌳 AST statements:', result.ast.statements.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: result.code,
|
||||||
|
lines: result.lines,
|
||||||
|
ast: result.ast,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error generating code from AST:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export for compatibility with existing code
|
// Export for compatibility with existing code
|
||||||
export const parser = new ANTLRSequenceParser();
|
export const parser = new ANTLRSequenceParser();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create ANTLR parser components for hybrid editor
|
||||||
|
*/
|
||||||
|
export function createSequenceParser(input: string): {
|
||||||
|
parser: SequenceParser;
|
||||||
|
tokenStream: CommonTokenStream;
|
||||||
|
} {
|
||||||
|
console.log('🔧 Creating ANTLR sequence parser components');
|
||||||
|
|
||||||
|
// Create lexer
|
||||||
|
const inputStream = CharStream.fromString(input);
|
||||||
|
const lexer = new SequenceLexer(inputStream);
|
||||||
|
|
||||||
|
// Create token stream
|
||||||
|
const tokenStream = new CommonTokenStream(lexer);
|
||||||
|
|
||||||
|
// Create parser
|
||||||
|
const parser = new SequenceParser(tokenStream);
|
||||||
|
|
||||||
|
// Configure error handling - remove default error listeners for cleaner output
|
||||||
|
parser.removeErrorListeners();
|
||||||
|
|
||||||
|
return { parser, tokenStream };
|
||||||
|
}
|
||||||
export default parser;
|
export default parser;
|
||||||
|
@@ -64,6 +64,50 @@ const newParser = {
|
|||||||
return jisonParser.parse(newSrc);
|
return jisonParser.parse(newSrc);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Expose AST-to-code generation functionality for browser access
|
||||||
|
generateCodeFromAST: () => {
|
||||||
|
if (USE_ANTLR_PARSER && antlrParser.generateCodeFromAST) {
|
||||||
|
return antlrParser.generateCodeFromAST();
|
||||||
|
}
|
||||||
|
console.warn('⚠️ AST-to-code generation only available with ANTLR parser');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
// Expose individual AST access methods for browser access
|
||||||
|
getAST: () => {
|
||||||
|
if (USE_ANTLR_PARSER && antlrParser.getAST) {
|
||||||
|
return antlrParser.getAST();
|
||||||
|
}
|
||||||
|
console.warn('⚠️ AST access only available with ANTLR parser');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getGeneratedCode: () => {
|
||||||
|
if (USE_ANTLR_PARSER && antlrParser.getGeneratedCode) {
|
||||||
|
return antlrParser.getGeneratedCode();
|
||||||
|
}
|
||||||
|
console.warn('⚠️ Generated code access only available with ANTLR parser');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getGeneratedLines: () => {
|
||||||
|
if (USE_ANTLR_PARSER && antlrParser.getGeneratedLines) {
|
||||||
|
return antlrParser.getGeneratedLines();
|
||||||
|
}
|
||||||
|
console.warn('⚠️ Generated lines access only available with ANTLR parser');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
// Expose formatting-preserving regeneration method
|
||||||
|
regenerateCodeWithFormatting: () => {
|
||||||
|
if (USE_ANTLR_PARSER && antlrParser.regenerateCodeWithFormatting) {
|
||||||
|
return antlrParser.regenerateCodeWithFormatting();
|
||||||
|
}
|
||||||
|
console.warn('⚠️ Formatting-preserving regeneration only available with ANTLR parser');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Expose parser globally for browser access (for AST regeneration testing)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
(window as any).MERMAID_SEQUENCE_PARSER = newParser;
|
||||||
|
console.log('🌐 Sequence parser exposed globally as window.MERMAID_SEQUENCE_PARSER');
|
||||||
|
}
|
||||||
|
|
||||||
export default newParser;
|
export default newParser;
|
||||||
|
Reference in New Issue
Block a user