UNPKG

meld

Version:

Meld: A template language for LLM prompts

371 lines (327 loc) 12.6 kB
import { IParserService } from './IParserService.js'; import type { MeldNode, CodeFenceNode, TextNode } from 'meld-spec'; import { parserLogger as logger } from '@core/utils/logger.js'; import { MeldParseError } from '@core/errors/MeldParseError.js'; import type { Location, Position } from '@core/types/index.js'; import { IStateService } from '@services/state/StateService/IStateService.js'; import { IResolutionService } from '@services/resolution/ResolutionService/IResolutionService.js'; import type { ResolutionContext } from '@services/resolution/ResolutionService/IResolutionService.js'; // Define our own ParseError type since it's not exported from meld-ast interface ParseError { message: string; location: { start: { line: number; column: number }; end: { line: number; column: number }; }; } interface MeldAstError extends Error { message: string; name: string; location?: { start: { line: number; column: number }; end: { line: number; column: number }; }; toString(): string; } function isMeldAstError(error: unknown): error is MeldAstError { return ( typeof error === 'object' && error !== null && 'message' in error && 'name' in error && typeof (error as any).toString === 'function' ); } export class ParserService implements IParserService { private resolutionService?: IResolutionService; constructor(resolutionService?: IResolutionService) { this.resolutionService = resolutionService; } setResolutionService(resolutionService: IResolutionService): void { this.resolutionService = resolutionService; } private async parseContent(content: string, filePath?: string): Promise<MeldNode[]> { try { const { parse } = await import('meld-ast'); const options = { failFast: true, trackLocations: true, validateNodes: true, preserveCodeFences: true, validateCodeFences: true, structuredPaths: true, onError: (error: unknown) => { if (isMeldAstError(error)) { logger.warn('Parse warning', { error: error.toString() }); } } }; // Register the content with source mapping service if a filePath is provided if (filePath) { try { const { registerSource } = require('@core/utils/sourceMapUtils.js'); registerSource(filePath, content); logger.debug(`Registered content for source mapping: ${filePath}`); } catch (err) { // Source mapping is optional, so just log a debug message if it fails logger.debug('Source mapping not available, skipping registration', { error: err }); } } const result = await parse(content, options); // Validate code fence nesting this.validateCodeFences(result.ast || []); // Log any non-fatal errors if (result.errors && result.errors.length > 0) { result.errors.forEach(error => { if (isMeldAstError(error)) { // Don't log warnings directly - we'll handle them through the error display service logger.debug('Parse warning detected', { errorMessage: error.toString() }); } }); } return result.ast || []; } catch (error) { if (isMeldAstError(error)) { // Create a MeldParseError with the original error information const errorLocation = error.location || { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }; // Always use the provided filePath if we have one, don't rely on what's in the error const actualFilePath = filePath; const locationWithPath = { ...errorLocation, filePath: actualFilePath }; const parseError = new MeldParseError( error.message, locationWithPath, { filePath: actualFilePath, // Directly set filePath in the error options cause: isMeldAstError(error) ? error : undefined, // Set the original error as the cause only if it's a proper Error context: { originalError: error, sourceLocation: { filePath: actualFilePath, line: errorLocation.start.line, column: errorLocation.start.column }, location: locationWithPath, // Add the file path in the context for the error display service to use errorFilePath: actualFilePath } } ); // Try to enhance with source mapping information if (filePath) { try { const { enhanceMeldErrorWithSourceInfo } = require('@core/utils/sourceMapUtils.js'); const enhancedError = enhanceMeldErrorWithSourceInfo(parseError); logger.debug('Enhanced parse error with source mapping', { original: parseError.message, enhanced: enhancedError.message, sourceLocation: enhancedError.context?.sourceLocation }); throw enhancedError; } catch (enhancementError) { // If enhancement fails, throw the original error logger.debug('Failed to enhance parse error with source mapping', { error: enhancementError }); throw parseError; } } throw parseError; } // For unknown errors, provide a generic message with proper location information const actualFilePath = filePath; const locationWithPath = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 }, filePath: actualFilePath }; const genericError = new MeldParseError( 'Parse error: Unknown error occurred', locationWithPath, { filePath: actualFilePath, // Directly set filePath in the error options cause: isMeldAstError(error) ? error : undefined, // Set the original error as the cause only if it's a proper Error context: { originalError: error, sourceLocation: { filePath: actualFilePath, line: 1, column: 1 }, location: locationWithPath, // Add the file path in the context for the error display service to use errorFilePath: actualFilePath } } ); // Try to enhance with source mapping information if (filePath) { try { const { enhanceMeldErrorWithSourceInfo } = require('@core/utils/sourceMapUtils.js'); throw enhanceMeldErrorWithSourceInfo(genericError); } catch (enhancementError) { // If enhancement fails, throw the original error throw genericError; } } throw genericError; } } public async parse(content: string, filePath?: string): Promise<MeldNode[]> { return this.parseContent(content, filePath); } public async parseWithLocations(content: string, filePath?: string): Promise<MeldNode[]> { const nodes = await this.parseContent(content, filePath); if (!filePath) { return nodes; } return nodes.map(node => { if (node.location) { // Preserve exact column numbers from original location return { ...node, location: { ...node.location, // Preserve all original location properties filePath // Only add filePath } }; } return node; }); } private isParseError(error: unknown): error is ParseError { return ( typeof error === 'object' && error !== null && 'message' in error && 'location' in error && typeof error.location === 'object' && error.location !== null && 'start' in error.location && 'end' in error.location ); } private validateCodeFences(nodes: MeldNode[]): void { // Since we're using the meld-ast parser with validateNodes=true and preserveCodeFences=true, // we can trust that the code fences are already valid. // This is just an extra validation layer to ensure code fence integrity for (const node of nodes) { if (node.type === 'CodeFence') { const codeFence = node as CodeFenceNode; const content = codeFence.content; // Skip empty code fences (should be rare but possible) if (!content) { continue; } // Split the content by lines const lines = content.split('\n'); if (lines.length < 2) { throw new MeldParseError( 'Invalid code fence: must have at least an opening and closing line', node.location || { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } } ); } // Get the first line (opening fence) and count backticks const firstLine = lines[0]; let openTickCount = 0; for (let i = 0; i < firstLine.length; i++) { if (firstLine[i] === '`') { openTickCount++; } else { break; } } // Get the last line (closing fence) and count backticks const lastLine = lines[lines.length - 1]; let closeTickCount = 0; for (let i = 0; i < lastLine.length; i++) { if (lastLine[i] === '`') { closeTickCount++; } else { break; } } if (openTickCount === 0) { throw new MeldParseError( 'Invalid code fence: missing opening backticks', node.location || { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } } ); } if (closeTickCount === 0) { throw new MeldParseError( 'Invalid code fence: missing closing backticks', node.location || { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } } ); } if (openTickCount !== closeTickCount) { throw new MeldParseError( `Code fence must be closed with exactly ${openTickCount} backticks, got ${closeTickCount}`, node.location || { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } } ); } } } } /** * Transform a variable node to its resolved value * Used for preview and transformation mode to resolve values * @param node The node to transform * @param state The state service to use for lookup * @returns A text node with the resolved value if transformation is enabled */ async transformVariableNode(node: MeldNode, state: IStateService): Promise<MeldNode> { // Only transform if transformation mode is enabled if (!state.isTransformationEnabled()) { return node; } // Ensure we have a resolution service if (!this.resolutionService) { logger.warn('No resolution service available for variable transformation'); return node; } // Create a simple resolution context const context: ResolutionContext = { state, currentFilePath: '/', strict: false, allowedVariableTypes: { text: true, data: true, path: true, command: false } }; try { // Handle different node types switch (node.type) { case 'TextVar': case 'DataVar': { // Extract variable name (simplified approach without serializer) let variableName = ''; if (node.type === 'TextVar' && 'name' in node) { variableName = `\${${(node as any).name}}`; } else if (node.type === 'DataVar' && 'name' in node) { variableName = `\${{${(node as any).name}}}`; } if (!variableName) { return node; } // Resolve the variable reference const resolved = await this.resolutionService.resolveInContext(variableName, context); // Create a new Text node with the resolved value const textNode: TextNode = { type: 'Text', content: resolved || '' }; // Copy location if available if (node.location) { textNode.location = node.location; } return textNode; } default: return node; } } catch (error) { logger.error('Error transforming variable node:', { error }); return node; } } }