/** * TemplateEngine * * Core template rendering engine for PDF generation. * Handles: * - Frontmatter parsing (YAML metadata) * - Handlebars template compilation * - Markdown to HTML conversion * - Layout system integration * - Template caching * - Multi-language support with fallback * * Pattern matches @plyaz/notifications/templates/TemplateEngine */ import Handlebars from 'handlebars'; import type { TemplateService, StorageTranslationService as StorageTranslationServiceInterface, StorageTemplateRenderResult, OUTPUT_FORMAT, PDFRenderOptions, TemplateVariableDefinition, TemplateVariableValidationResult, TemplateValidationOptions, StorageTemplateRenderResultWithValidation } from '@plyaz/types/storage'; import type { StorageTemplateEngineConfig } from '@plyaz/types/storage'; import type { RendererRegistry } from '../renderers/core/RendererRegistry'; import type { LayoutEngine } from './LayoutEngine'; /** * TemplateEngine * * Renders markdown templates with Handlebars variables and frontmatter. * Supports layouts, custom helpers, and multi-language templates. * * @example * ```typescript * const engine = new TemplateEngine({ * templateService: new FileSystemTemplateService(), * defaultLocale: 'en', * cacheEnabled: true * }); * * const result = await engine.render('invoices/standard-invoice', { * invoiceNumber: 'INV-001', * amount: 1000 * }, 'en'); * ``` */ export declare class TemplateEngine { private readonly handlebars; private readonly templateService; private readonly translationService?; private readonly rendererRegistry?; private readonly layoutEngine?; private readonly logger?; private readonly templateCache; private readonly compiledCache; private defaultLocale; private cacheEnabled; private cacheTTL; constructor(config: StorageTemplateEngineConfig); /** * Render template with data * * @param templateId - Template identifier (category/templateName) * @param data - Template data (variables) * @param locale - Locale (default: defaultLocale) * @returns Rendered HTML with frontmatter * * @example * ```typescript * const result = await engine.render('invoices/standard-invoice', { * invoiceNumber: 'INV-001', * customer: { name: 'John Doe' }, * items: [{ name: 'Product A', price: 100 }], * total: 100 * }, 'en'); * * // With validation * const result = await engine.render('invoices/standard-invoice', data, 'en', { * validation: { * strict: true, // throws if validation fails * schema: zodSchema, // optional custom Zod schema * } * }); * ``` */ render(templateId: string, data: Record, locale?: string, options?: { validation?: TemplateValidationOptions; }): Promise; /** * Validate template data and return coerced values * * @param templateId - Template identifier for error context * @param data - Raw template data * @param templateEntry - Loaded template with frontmatter * @param options - Validation options * @returns Validated data result with render data and optional validation result * @internal */ private validateTemplateData; /** * Check if data validation should be performed * * @param templateEntry - Loaded template entry * @param options - Validation options * @returns True if validation should be performed * @internal */ private shouldValidateData; /** * Handle validation result - throw if strict mode or log warnings * * @param templateId - Template ID for logging * @param result - Validation result * @param strict - Whether to throw on validation failure * @throws StoragePackageError if strict mode and validation failed * @internal */ private handleValidationResult; /** * Render template entry to HTML with layout application * * @param options - Render options including cache key, template entry, and data * @returns Rendered HTML string * @internal */ private renderTemplateToHtml; /** * Apply layout components if configured in frontmatter */ private applyLayoutIfConfigured; /** * Handle render errors uniformly */ private handleRenderError; /** * Load template from service with caching and fallback * * @param templateId - Template identifier * @param locale - Locale * @returns Template cache entry */ private loadTemplate; /** * Resolve template path with locale fallback * * Tries in order: * 1. {locale}/{templateId}.md (e.g., en-US/invoices/standard.md) * 2. {baseLocale}/{templateId}.md (e.g., en/invoices/standard.md) * 3. {defaultLocale}/{templateId}.md (e.g., en/invoices/standard.md) * * @param templateId - Template identifier * @param locale - Requested locale * @returns Resolved template path */ private resolveTemplatePath; /** * Register a custom Handlebars helper * * @param name - Helper name * @param helper - Helper function * * @example * ```typescript * engine.registerHelper('uppercase', (str: string) => str.toUpperCase()); * ``` */ registerHelper(name: string, helper: Handlebars.HelperDelegate): void; /** * Register multiple Handlebars helpers * * @param helpers - Map of helper name to function */ registerHelpers(helpers: Record): void; /** * Clear template cache */ clearCache(): void; /** * Clear specific template from cache * * @param templateId - Template identifier * @param locale - Locale (optional, clears all locales if not provided) */ clearTemplateCache(templateId: string, locale?: string): void; /** * Get cache statistics */ getCacheStats(): { templateCacheSize: number; compiledCacheSize: number; cacheEnabled: boolean; cacheTTL: number; }; /** * Set default locale for template rendering * * @param locale - Locale code (e.g., 'en', 'es', 'fr') * * @example * ```typescript * engine.setDefaultLocale('es'); // Set Spanish as default * ``` */ setDefaultLocale(locale: string): void; /** * Get current default locale * * @returns Current default locale code */ getDefaultLocale(): string; /** * Enable or disable template caching * * @param enabled - Whether to enable caching */ setCacheEnabled(enabled: boolean): void; /** * Set cache TTL * * @param ttl - Time to live in milliseconds */ setCacheTTL(ttl: number): void; /** * Get translation string content (requires translationService) * * @param path - Translation file path (e.g., 'en/storage.json') * @returns Translation JSON string * @throws StoragePackageError if translation service not configured or file not found * * @example * ```typescript * const translations = await engine.getTranslation('en/storage.json'); * const data = JSON.parse(translations); * ``` */ getTranslation(path: string): Promise; /** * Check if translation exists (requires translationService) * * @param path - Translation file path * @returns true if translation exists */ hasTranslation(path: string): Promise; /** * Get translation service instance * * @returns Translation service or undefined if not configured */ getTranslationService(): StorageTranslationServiceInterface | undefined; /** * Render template to PDF (convenience method) * * Complete flow: Template (markdown) → HTML → PDF * * @param options - PDF render options * @param options.templateId - Template identifier * @param options.data - Template data * @param options.locale - Locale (optional, defaults to defaultLocale) * @param options.pdfOptions - PDF-specific options (optional) * @param options.validation - Validation options (optional) * @returns PDF as Buffer * @throws StoragePackageError if renderer registry not configured * * @example * ```typescript * const pdf = await engine.renderToPDF({ * templateId: 'invoices/standard-invoice', * data: { invoiceNumber: 'INV-12345', customer: { name: 'John Doe' }, total: 1000 }, * locale: 'en', * pdfOptions: { pageSize: 'A4', orientation: 'portrait' } * }); * ``` */ renderToPDF(options: { templateId: string; data: Record; locale?: string; pdfOptions?: PDFRenderOptions; validation?: TemplateValidationOptions; }): Promise; /** * Render template to specific output format * * Complete flow: Template (markdown) → HTML → Renderer → Output format * * @param templateId - Template identifier * @param data - Template data * @param format - Output format (pdf, png, jpeg, etc.) * @param locale - Locale (optional, defaults to defaultLocale) * @param pdfOptions - PDF-specific options (optional, only used for PDF format) * @returns Rendered content as Buffer * @throws StoragePackageError if renderer registry not configured or format not supported * * @example * ```typescript * // Render to PDF * const pdf = await engine.renderToFormat('invoices/standard', data, OUTPUT_FORMAT.PDF, 'en'); * * // Render to PNG (screenshot of template) * const png = await engine.renderToFormat('certificates/award', data, OUTPUT_FORMAT.PNG, 'en'); * ``` */ renderToFormat(options: { templateId: string; data: Record; format: OUTPUT_FORMAT; locale?: string; pdfOptions?: PDFRenderOptions; validation?: TemplateValidationOptions; }): Promise; /** * Get renderer registry instance * * @returns Renderer registry or undefined if not configured */ getRendererRegistry(): RendererRegistry | undefined; /** * Normalize HTML indentation in markdown to prevent code block issues * * Markdown treats 4+ spaces as code blocks. This is problematic for templates * with indented HTML after Handlebars conditionals. This method removes leading * spaces from lines that start with HTML tags, EXCEPT when inside fenced code blocks. * * For actual code examples in templates, use fenced code blocks (triple backticks) * instead of indentation: * * ```html *

This will be preserved as code

* ``` * * @param markdown - Markdown content with potential indented HTML * @returns Normalized markdown with HTML indentation removed outside of fenced blocks */ private normalizeHtmlIndentation; /** * Perform validation using custom schema or frontmatter variables */ private performValidation; /** Validate using custom validate() function */ private validateWithCustomValidator; /** Validate using Zod safeParse() */ private validateWithSafeParse; /** Validate using Zod parse() with try/catch */ private validateWithParse; /** Convert Zod issues to error record */ private convertZodIssuesToErrors; /** Merge frontmatter and additional variables */ private mergeVariableDefinitions; /** * Validate template data against variable schema defined in frontmatter * * @param templateId - Template identifier * @param data - Data to validate * @param locale - Locale (optional) * @returns Validation result with errors and coerced values * * @example * ```typescript * const result = await engine.validateData('invoices/standard-invoice', { * invoiceNumber: 'INV-001', * taxRate: '10', // will be coerced to number * }); * * if (!result.valid) { * console.log('Errors:', result.errors); * } * ``` */ validateData(templateId: string, data: Record, locale?: string): Promise; /** * Validate data against a variable schema * * @param data - Data to validate * @param variables - Variable definitions * @returns Validation result */ validateDataAgainstSchema(data: Record, variables: TemplateVariableDefinition[]): TemplateVariableValidationResult; /** * Validate and coerce value to expected type based on variable definition * * @param varDef - Variable definition with type and validation rules * @param value - Value to validate and coerce * @returns Coercion result with coerced value, errors, and warnings * @internal */ private validateAndCoerceType; /** @internal Coerce value to string */ private coerceString; /** @internal Coerce value to number */ private coerceNumber; /** @internal Coerce value to boolean */ private coerceBoolean; /** @internal Coerce value to Date */ private coerceDate; /** @internal Validate email format */ private coerceEmail; /** @internal Validate phone format */ private coercePhone; /** @internal Validate URL format */ private coerceUrl; /** @internal Validate array type */ private coerceArray; /** @internal Validate object type */ private coerceObject; /** @internal Validate select value against enum */ private coerceSelect; /** @internal Validate multiselect values against enum */ private coerceMultiselect; /** @internal Validate image URL or base64 */ private coerceImage; /** * Apply validation rules to a value */ private applyValidationRules; private validateString; private validateNumber; private validateDate; private validateArray; /** * Render with validation - validates data before rendering * * @param templateId - Template identifier * @param data - Template data * @param locale - Locale * @param options - Options including strict mode * @returns Rendered result * @throws StoragePackageError if validation fails in strict mode * * @example * ```typescript * // Strict mode - throws if validation fails * const result = await engine.renderWithValidation( * 'invoices/standard-invoice', * data, * 'en', * { strict: true } * ); * * // Non-strict mode - renders with warnings but uses coerced values * const result = await engine.renderWithValidation( * 'invoices/standard-invoice', * data, * 'en', * { strict: false } * ); * ``` */ renderWithValidation(templateId: string, data: Record, locale?: string, options?: { strict?: boolean; }): Promise; }