UNPKG

meld

Version:

Meld: A template language for LLM prompts

149 lines (128 loc) 5.36 kB
import { ResolutionError } from '@services/resolution/ResolutionService/errors/ResolutionError.js'; import { StringLiteralHandler } from './StringLiteralHandler.js'; import { IResolutionService } from '@services/resolution/ResolutionService/IResolutionService.js'; import { ResolutionContext } from '@services/resolution/ResolutionService/IResolutionService.js'; import type { IParserService } from '@services/pipeline/ParserService/IParserService.js'; import type { MeldNode, TextNode } from 'meld-spec'; /** * Handles string concatenation operations using the ++ operator */ export class StringConcatenationHandler { private stringLiteralHandler: StringLiteralHandler; constructor( private resolutionService: IResolutionService, private parserService?: IParserService ) { this.stringLiteralHandler = new StringLiteralHandler(); } /** * Splits a value into its concatenation parts * @returns Array of parts to be concatenated * @throws ResolutionError if the concatenation syntax is invalid */ private async splitConcatenationParts(value: string): Promise<string[]> { // If ParserService is available, try to use it for more accurate parsing if (this.parserService) { try { // Create a simple element to parse with the concatenation // Add some context to make it valid Meld syntax const wrappedValue = `@text test = ${value}`; // Parse with AST const nodes = await this.parserService.parse(wrappedValue); // Look for directive nodes with concatenation operators const directiveNode = nodes.find(node => node.type === 'Directive' && (node as any).directive?.kind === 'text' ); if (directiveNode) { // Access the value which should contain our concatenation const directiveValue = (directiveNode as any).directive?.value; // If the parser properly recognized the concatenation if (directiveValue && typeof directiveValue === 'object' && directiveValue.type === 'Concatenation') { // Return the parts directly from the AST return directiveValue.parts.map((p: any) => typeof p === 'object' && p.raw ? p.raw : String(p) ); } } // If AST parsing didn't identify concatenation structure, // fall back to simpler splitting } catch (error) { console.warn('Failed to parse concatenation with AST, falling back to manual parsing:', error); } } // Fallback: Split by ++ operator, preserving spaces around it const parts = value.split(/\s*\+\+\s*/); // Validate each part is non-empty if (parts.some(part => part.trim().length === 0)) { throw new ResolutionError( 'Empty part in string concatenation', { value } ); } return parts; } /** * Checks if a value contains the ++ operator */ async hasConcatenation(value: string): Promise<boolean> { // Try to use the parser to detect concatenation if available if (this.parserService) { try { // Wrap the value for parsing const wrappedValue = `@text test = ${value}`; // Parse the wrapped value const nodes = await this.parserService.parse(wrappedValue); // Look for directive nodes with concatenation operators const directiveNode = nodes.find(node => node.type === 'Directive' && (node as any).directive?.kind === 'text' ); if (directiveNode) { // Check if the parser recognized a Concatenation node const directiveValue = (directiveNode as any).directive?.value; return directiveValue && typeof directiveValue === 'object' && directiveValue.type === 'Concatenation'; } } catch (error) { // If parsing fails, fall back to regex check console.warn('Failed to check concatenation with AST, falling back to regex:', error); } } // Fallback: Look for ++ with required spaces on both sides return /\s\+\+\s/.test(value); } /** * Resolves a string concatenation expression * @throws ResolutionError if the concatenation is invalid */ async resolveConcatenation(value: string, context: ResolutionContext): Promise<string> { // Split into parts const parts = await this.splitConcatenationParts(value); // Resolve each part const resolvedParts: string[] = []; for (const part of parts) { const trimmedPart = part.trim(); // Handle string literals if (this.stringLiteralHandler.isStringLiteral(trimmedPart)) { resolvedParts.push(this.stringLiteralHandler.parseLiteral(trimmedPart)); continue; } // Handle variables and other expressions try { const resolved = await this.resolutionService.resolveInContext(trimmedPart, context); resolvedParts.push(resolved); } catch (error) { throw new ResolutionError( `Failed to resolve part in concatenation: ${trimmedPart}`, { value: trimmedPart, context, cause: error } ); } } // Join all parts return resolvedParts.join(''); } }