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 { SequenceListener } from './SequenceListener.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
|
||||
@@ -190,6 +192,49 @@ export class ANTLRSequenceParser {
|
||||
// eslint-disable-next-line no-console
|
||||
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;
|
||||
} catch (error) {
|
||||
const totalTime = performance.now() - startTime;
|
||||
@@ -205,8 +250,485 @@ export class ANTLRSequenceParser {
|
||||
setYY(yy: any) {
|
||||
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 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;
|
||||
|
@@ -64,6 +64,50 @@ const newParser = {
|
||||
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;
|
||||
|
Reference in New Issue
Block a user