testing with antlr code generation

This commit is contained in:
Ashish Jain
2025-09-26 14:12:46 +02:00
parent fa75f8de77
commit 752138e9ba
9 changed files with 3776 additions and 69 deletions

331
demos/class-antlr-test.html Normal file
View 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>

View 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

View File

@@ -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 = [];
}
}

View File

@@ -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');
}
}

View File

@@ -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
};
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;