/ src / services / canvas-parser-service.ts
canvas-parser-service.ts
  1  import { VaultService } from './vault-service';
  2  
  3  // Simple path normalization for cross-platform compatibility
  4  function normalizePath(path: string): string {
  5    return path.replace(/\\/g, '/').replace(/\/+/g, '/');
  6  }
  7  
  8  export interface CanvasNode {
  9    id: string;
 10    type: 'file' | 'text' | 'group' | 'link';
 11    x: number;
 12    y: number;
 13    width: number;
 14    height: number;
 15    color?: string;
 16    file?: string; // Path for file nodes
 17    text?: string; // Content for text nodes
 18    url?: string; // URL for link nodes
 19  }
 20  
 21  export interface CanvasEdge {
 22    id: string;
 23    fromNode: string;
 24    toNode: string;
 25    fromSide?: 'top' | 'right' | 'bottom' | 'left';
 26    toSide?: 'top' | 'right' | 'bottom' | 'left';
 27    toEnd?: 'none' | 'arrow'; // For undirected edges (toEnd: 'none')
 28    color?: string;
 29    label?: string;
 30  }
 31  
 32  export interface CanvasData {
 33    nodes: CanvasNode[];
 34    edges: CanvasEdge[];
 35  }
 36  
 37  export interface DependencyInfo {
 38    filePath: string;
 39    nodeId: string;
 40    isExternal: boolean; // Outside current DreamNode boundary
 41    dreamNodePath?: string; // Path to containing DreamNode if external
 42  }
 43  
 44  export interface CanvasAnalysis {
 45    canvasPath: string;
 46    dreamNodeBoundary: string; // Path to DreamNode containing this canvas
 47    dependencies: DependencyInfo[];
 48    externalDependencies: DependencyInfo[];
 49    hasExternalDependencies: boolean;
 50  }
 51  
 52  export class CanvasParserService {
 53    private boundaryCache = new Map<string, string>();
 54  
 55    constructor(private vaultService: VaultService) {}
 56  
 57    /**
 58     * Parse a canvas file and return structured data
 59     */
 60    async parseCanvas(canvasPath: string): Promise<CanvasData> {
 61      try {
 62        const canvasContent = await this.vaultService.readFile(canvasPath);
 63        const canvasData = JSON.parse(canvasContent) as CanvasData;
 64        
 65        // Validate basic structure
 66        if (!canvasData.nodes || !Array.isArray(canvasData.nodes)) {
 67          throw new Error('Invalid canvas format: missing or invalid nodes array');
 68        }
 69        
 70        if (!canvasData.edges || !Array.isArray(canvasData.edges)) {
 71          throw new Error('Invalid canvas format: missing or invalid edges array');
 72        }
 73  
 74        return canvasData;
 75      } catch (error) {
 76        if (error instanceof SyntaxError) {
 77          throw new Error(`Invalid canvas JSON in ${canvasPath}: ${error.message}`);
 78        }
 79        throw error;
 80      }
 81    }
 82  
 83    /**
 84     * Find the DreamNode boundary for a given path by walking up the directory tree
 85     * looking for a .udd file
 86     */
 87    async findDreamNodeBoundary(filePath: string): Promise<string | null> {
 88      const normalizedPath = normalizePath(filePath);
 89      
 90      console.log(`🔍 [Canvas Parser] Finding boundary for: "${filePath}"`);
 91      console.log(`🔍 [Canvas Parser] Normalized path: "${normalizedPath}"`);
 92      
 93      // Check cache first
 94      if (this.boundaryCache.has(normalizedPath)) {
 95        return this.boundaryCache.get(normalizedPath) || null;
 96      }
 97  
 98      let currentPath = normalizedPath;
 99      
100      // Handle file vs directory paths - get the directory containing the file
101      if (await this.vaultService.fileExists(currentPath)) {
102        // It's a file, start from parent directory (the directory containing the file)
103        currentPath = currentPath.split('/').slice(0, -1).join('/');
104        console.log(`📁 [Canvas Parser] File detected, checking directory: "${currentPath}"`);
105      }
106  
107      // Walk up directory tree starting from the file's directory
108      let attempts = 0;
109      while (currentPath && currentPath !== '.' && currentPath !== '' && attempts < 10) {
110        const uddPath = normalizePath(`${currentPath}/.udd`);
111        console.log(`🔍 [Canvas Parser] Attempt ${attempts + 1}: Checking for .udd at: "${uddPath}"`);
112        
113        const exists = await this.vaultService.fileExists(uddPath);
114        console.log(`${exists ? '✅' : '❌'} [Canvas Parser] .udd file ${exists ? 'FOUND' : 'NOT FOUND'} at: "${uddPath}"`);
115        
116        // Debug: List what files ARE in this directory
117        if (!exists && attempts === 0) {
118          // console.log(`🔍 [Canvas Parser] DEBUG: Listing files in directory "${currentPath}"`); // Debug removed for production
119          try {
120            const folder = this.vaultService.obsidianVault.getAbstractFileByPath(currentPath);
121            if (folder && 'children' in folder) {
122              const children = (folder as { children?: { name: string }[] }).children || [];
123              console.log(`📁 [Canvas Parser] Found ${children.length} items in "${currentPath}":`);
124              children.forEach((child: { name: string }, i: number) => {
125                console.log(`  ${i + 1}. "${child.name}" (${child.constructor.name})`);
126              });
127            } else {
128              console.log(`❌ [Canvas Parser] Could not access folder: "${currentPath}"`);
129            }
130          } catch (error) {
131            console.log(`❌ [Canvas Parser] Error listing directory: ${error}`);
132          }
133        }
134        
135        if (exists) {
136          this.boundaryCache.set(normalizedPath, currentPath);
137          console.log(`🎯 [Canvas Parser] Boundary found: "${currentPath}"`);
138          return currentPath;
139        }
140        
141        // Move up one directory
142        const pathParts = currentPath.split('/');
143        if (pathParts.length <= 1) break;
144        currentPath = pathParts.slice(0, -1).join('/');
145        console.log(`⬆️ [Canvas Parser] Moving up to: "${currentPath}"`);
146        attempts++;
147      }
148  
149      // Check root level
150      const rootUddPath = '.udd';
151      console.log(`🔍 [Canvas Parser] Checking root level: "${rootUddPath}"`);
152      const rootExists = await this.vaultService.fileExists(rootUddPath);
153      console.log(`${rootExists ? '✅' : '❌'} [Canvas Parser] Root .udd ${rootExists ? 'FOUND' : 'NOT FOUND'}`);
154      
155      if (rootExists) {
156        this.boundaryCache.set(normalizedPath, '');
157        return '';
158      }
159  
160      // No boundary found
161      console.log(`❌ [Canvas Parser] No .udd file found for: "${filePath}"`);
162      this.boundaryCache.set(normalizedPath, '');
163      return null;
164    }
165  
166    /**
167     * Analyze canvas dependencies and identify external references
168     */
169    async analyzeCanvasDependencies(canvasPath: string): Promise<CanvasAnalysis> {
170      const canvasData = await this.parseCanvas(canvasPath);
171      const canvasBoundary = await this.findDreamNodeBoundary(canvasPath);
172      
173      if (!canvasBoundary) {
174        throw new Error(`Canvas ${canvasPath} is not inside a DreamNode (no .udd file found)`);
175      }
176  
177      const dependencies: DependencyInfo[] = [];
178      
179      // Process file nodes
180      for (const node of canvasData.nodes) {
181        if (node.type === 'file' && node.file) {
182          const filePath = normalizePath(node.file);
183          const fileBoundary = await this.findDreamNodeBoundary(filePath);
184          
185          const isExternal = fileBoundary !== canvasBoundary;
186          
187          const dependencyInfo: DependencyInfo = {
188            filePath,
189            nodeId: node.id,
190            isExternal,
191            dreamNodePath: isExternal ? fileBoundary || undefined : undefined
192          };
193          
194          dependencies.push(dependencyInfo);
195        }
196      }
197  
198      const externalDependencies = dependencies.filter(dep => dep.isExternal);
199  
200      return {
201        canvasPath,
202        dreamNodeBoundary: canvasBoundary,
203        dependencies,
204        externalDependencies,
205        hasExternalDependencies: externalDependencies.length > 0
206      };
207    }
208  
209    /**
210     * Get all file nodes from a canvas
211     */
212    async getFileNodes(canvasPath: string): Promise<CanvasNode[]> {
213      const canvasData = await this.parseCanvas(canvasPath);
214      return canvasData.nodes.filter(node => node.type === 'file');
215    }
216  
217    /**
218     * Update canvas file paths (for submodule rewriting)
219     */
220    async updateCanvasFilePaths(canvasPath: string, pathUpdates: Map<string, string>): Promise<void> {
221      const canvasData = await this.parseCanvas(canvasPath);
222      let modified = false;
223  
224      // Update file paths in nodes
225      for (const node of canvasData.nodes) {
226        if (node.type === 'file' && node.file) {
227          const normalizedPath = normalizePath(node.file);
228          if (pathUpdates.has(normalizedPath)) {
229            node.file = pathUpdates.get(normalizedPath)!;
230            modified = true;
231          }
232        }
233      }
234  
235      if (modified) {
236        const updatedContent = JSON.stringify(canvasData, null, 2);
237        await this.vaultService.writeFile(canvasPath, updatedContent);
238      }
239    }
240  
241    /**
242     * Clear the boundary detection cache
243     */
244    clearCache(): void {
245      this.boundaryCache.clear();
246    }
247  
248    /**
249     * Get a summary report of canvas analysis
250     */
251    generateAnalysisReport(analysis: CanvasAnalysis): string {
252      const { canvasPath, dreamNodeBoundary, dependencies, externalDependencies } = analysis;
253      
254      let report = `Canvas Analysis: ${canvasPath}\n`;
255      report += `DreamNode Boundary: ${dreamNodeBoundary || 'ROOT'}\n`;
256      report += `Total Dependencies: ${dependencies.length}\n`;
257      report += `External Dependencies: ${externalDependencies.length}\n\n`;
258      
259      if (externalDependencies.length > 0) {
260        report += 'External Dependencies:\n';
261        for (const dep of externalDependencies) {
262          report += `  - ${dep.filePath} (in ${dep.dreamNodePath || 'vault root'})\n`;
263        }
264      } else {
265        report += 'No external dependencies found.\n';
266      }
267      
268      return report;
269    }
270  }