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 }