UNPKG

legal-markdown-js

Version:

Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version

465 lines 16.8 kB
/** * Pipeline Configuration for Legal Markdown Processing * * This module provides the default pipeline configuration and factory functions * for creating Legal Markdown processing pipelines. It connects existing processors * with the new pipeline management system while maintaining full backward compatibility. * * Features: * - Default pipeline configuration matching current processing order * - Processor wrapper classes implementing BaseProcessor interface * - Factory functions for different processing contexts (HTML, PDF, markdown) * - Configuration validation and error handling * - Support for custom pipeline configurations * - Integration with existing Legal Markdown processors * * @example * ```typescript * import { createDefaultPipeline, createHtmlPipeline } from './pipeline-config.js'; * * // Create standard pipeline * const pipeline = createDefaultPipeline(); * * // Create pipeline optimized for HTML generation * const htmlPipeline = createHtmlPipeline({ * enableFieldTracking: true, * includeHighlighting: true * }); * * // Execute processing * const result = await pipeline.execute(content, metadata, options); * ``` * * @module */ import { AbstractProcessor } from '../../core/index.js'; import { parseYamlFrontMatter } from '../../core/parsers/yaml-parser.js'; import { processHeaders } from '../../core/processors/header-processor.js'; import { processOptionalClauses } from '../../core/processors/clause-processor.js'; import { processCrossReferences } from '../../core/processors/reference-processor.js'; import { processPartialImports } from '../../core/processors/import-processor.js'; import { processMixins } from '../ast-mixin-processor.js'; import { processTemplateLoops } from '../template-loops.js'; import { exportMetadata } from '../../core/exporters/metadata-exporter.js'; import { convertRstToLegalMarkdownSync } from '../rst-parser.js'; import { convertLatexToLegalMarkdownSync } from '../latex-parser.js'; import { fieldTracker } from '../tracking/field-tracker.js'; import { PipelineManager } from './pipeline-manager.js'; import { ConsolePipelineLogger, LogLevel } from './pipeline-logger.js'; /** * Wrapper for RST conversion processor */ class RstProcessor extends AbstractProcessor { name = 'rst-conversion'; isEnabled() { return true; // RST conversion is always enabled as a preprocessing step } performProcessing(content, metadata, options) { try { // Use sync version for now to match AbstractProcessor signature return convertRstToLegalMarkdownSync(content); } catch (error) { this.debug('RST conversion failed', error.message); return content; // Return original content if conversion fails } } } /** * Wrapper for LaTeX conversion processor */ class LatexProcessor extends AbstractProcessor { name = 'latex-conversion'; isEnabled() { return true; // LaTeX conversion is always enabled as a preprocessing step } performProcessing(content, metadata, options) { try { // Use sync version for now to match AbstractProcessor signature return convertLatexToLegalMarkdownSync(content); } catch (error) { this.debug('LaTeX conversion failed', error.message); return content; // Return original content if conversion fails } } } /** * Wrapper for YAML front matter parsing processor */ class YamlProcessor extends AbstractProcessor { name = 'yaml-parsing'; isEnabled() { return true; // YAML parsing is always enabled } performProcessing(content, metadata, options) { const result = parseYamlFrontMatter(content, options.throwOnYamlError); // Merge parsed metadata into the provided metadata object Object.assign(metadata, result.metadata || {}); // Return content without YAML front matter return result.content; } } /** * Wrapper for partial imports processor with frontmatter merge support */ class ImportsProcessor extends AbstractProcessor { name = 'imports'; isEnabled(options) { return !options.noImports; } performProcessing(content, metadata, options) { // Use enhanced import processor with frontmatter merge support const result = processPartialImports(content, options.basePath, metadata, options); // Store imported files in metadata for later reporting if (result.importedFiles && result.importedFiles.length > 0) { metadata._importedFiles = (metadata._importedFiles || []).concat(result.importedFiles); } // Merge imported frontmatter unless explicitly disabled if (options.disableFrontmatterMerge !== true && result.mergedMetadata) { // Merge the imported metadata into the current metadata // The processPartialImports function already applies "source wins" strategy, // so we can safely assign the merged metadata Object.assign(metadata, result.mergedMetadata); if (options.logImportOperations) { this.debug('Pipeline: Integrated merged frontmatter', `Added ${Object.keys(result.mergedMetadata).length} metadata fields from imports`); } } return result.content; } } /** * Wrapper for optional clauses processor */ class ClausesProcessor extends AbstractProcessor { name = 'clauses'; isEnabled(options) { return !options.noClauses; } performProcessing(content, metadata, options) { return processOptionalClauses(content, metadata); } } /** * Wrapper for cross-references processor */ class ReferencesProcessor extends AbstractProcessor { name = 'references'; isEnabled(options) { return !options.noReferences; } performProcessing(content, metadata, options) { return processCrossReferences(content, metadata); } } /** * Wrapper for mixins processor (AST-based) */ class MixinsProcessor extends AbstractProcessor { name = 'mixins'; isEnabled(options) { return !options.noMixins; } performProcessing(content, metadata, options) { this.debug('Processing mixins with AST processor', { contentLength: content.length, enableFieldTracking: options.enableFieldTrackingInMarkdown, }); return processMixins(content, metadata, options); } } /** * Wrapper for template loops processor */ class TemplateLoopsProcessor extends AbstractProcessor { name = 'template-loops'; isEnabled() { return true; // Template loops are always processed if present } performProcessing(content, metadata, options) { this.debug('Processing template loops', { contentLength: content.length, enableFieldTracking: options.enableFieldTrackingInMarkdown, }); const result = processTemplateLoops(content, metadata, undefined, options.enableFieldTrackingInMarkdown || false); // Debug: log result of template loops processing if (process.env.NODE_ENV === 'development') { console.log('Template loops result:', result.substring(0, 500)); } return result; } } /** * Wrapper for headers processor */ class HeadersProcessor extends AbstractProcessor { name = 'headers'; isEnabled(options) { return !options.noHeaders; } performProcessing(content, metadata, options) { return processHeaders(content, metadata, { noReset: options.noReset, noIndent: options.noIndent, enableFieldTrackingInMarkdown: options.enableFieldTrackingInMarkdown, }); } } /** * Wrapper for metadata export processor */ class MetadataExportProcessor extends AbstractProcessor { name = 'metadata-export'; isEnabled(options) { return !!(options.exportMetadata || options['meta-yaml-output'] || options['meta-json-output']); } performProcessing(content, metadata, options) { const exportResult = exportMetadata(metadata, options.exportFormat, options.exportPath); // Store exported files in metadata for later reporting if (exportResult.exportedFiles && exportResult.exportedFiles.length > 0) { metadata._exportedFiles = (metadata._exportedFiles || []).concat(exportResult.exportedFiles); } return content; // Metadata export doesn't modify content } } /** * Wrapper for final field tracking processor (centralized mode) */ class FieldTrackingProcessor extends AbstractProcessor { name = 'field-tracking'; isEnabled(options) { // Apply centralized field tracking if: // 1. Field tracking is explicitly enabled (user choice for markdown->markdown), OR // 2. We're generating HTML/PDF (always needed for CSS highlighting classes) // Note: Both HTML and PDF generation set _htmlGeneration flag return !!(options.enableFieldTracking || options._htmlGeneration); } performProcessing(content, metadata, options) { this.debug('Applying centralized field tracking', { fieldsCount: fieldTracker.getFields().size, contentLength: content.length, }); return fieldTracker.applyFieldTracking(content); } } /** * Create the default Legal Markdown processing pipeline * * This pipeline matches the exact order and behavior of the current processing * system, ensuring 100% backward compatibility. * * @param logger - Optional custom logger * @returns Configured pipeline manager * @example * ```typescript * const pipeline = createDefaultPipeline(); * const result = await pipeline.execute(content, metadata, { * legalMarkdownOptions: { enableFieldTracking: true } * }); * ``` */ export function createDefaultPipeline(logger) { const pipelineLogger = logger || new ConsolePipelineLogger({ level: process.env.NODE_ENV === 'development' ? LogLevel.DEBUG : LogLevel.ERROR, enableMetrics: process.env.NODE_ENV === 'development', enableColors: true, }); const pipeline = new PipelineManager(pipelineLogger); // Define steps in the exact order of current processing const steps = [ { name: 'rst-conversion', processor: new RstProcessor(), order: 1, enabled: true, description: 'Convert RST content to Legal Markdown format', }, { name: 'latex-conversion', processor: new LatexProcessor(), order: 2, enabled: true, description: 'Convert LaTeX content to Legal Markdown format', }, { name: 'yaml-parsing', processor: new YamlProcessor(), order: 3, enabled: true, description: 'Parse YAML front matter and extract metadata', }, { name: 'imports', processor: new ImportsProcessor(), order: 4, enabled: true, description: 'Process partial document imports', }, { name: 'clauses', processor: new ClausesProcessor(), order: 5, enabled: true, description: 'Process optional clauses and conditional blocks', }, { name: 'references', processor: new ReferencesProcessor(), order: 6, enabled: true, description: 'Resolve cross-references and internal links', }, { name: 'template-loops', processor: new TemplateLoopsProcessor(), order: 7, enabled: true, description: 'Process template loops and array iterations', }, { name: 'mixins', processor: new MixinsProcessor(), order: 8, enabled: true, description: 'Process mixins and variable substitutions (AST-based)', }, { name: 'headers', processor: new HeadersProcessor(), order: 9, enabled: true, description: 'Process headers, numbering, and formatting', }, { name: 'metadata-export', processor: new MetadataExportProcessor(), order: 10, enabled: true, description: 'Export metadata to external files if configured', }, { name: 'field-tracking', processor: new FieldTrackingProcessor(), order: 11, enabled: true, description: 'Apply centralized field tracking and highlighting', }, ]; // Register all steps steps.forEach(step => pipeline.registerStep(step)); return pipeline; } /** * Create a pipeline optimized for HTML generation * * @param options - HTML generation options * @returns Configured pipeline manager */ export function createHtmlPipeline(options = {}) { const pipeline = createDefaultPipeline(); // Add HTML-specific configuration // Always enable field tracking for HTML generation (headers, mixins, etc.) pipeline.addListener({ onPipelineStart: config => { config.fieldTrackingMode = 'distributed'; }, }); return pipeline; } /** * Create a pipeline optimized for PDF generation * * @param options - PDF generation options * @returns Configured pipeline manager */ export function createPdfPipeline(options = {}) { // PDF generation uses the same pipeline as HTML return createHtmlPipeline(options); } /** * Create a minimal pipeline for basic markdown processing * * @returns Configured pipeline manager with only essential steps */ export function createMinimalPipeline() { const pipeline = new PipelineManager(); const steps = [ { name: 'yaml-parsing', processor: new YamlProcessor(), order: 1, enabled: true, description: 'Parse YAML front matter', }, { name: 'mixins', processor: new MixinsProcessor(), order: 2, enabled: true, description: 'Process basic variable substitutions', }, ]; steps.forEach(step => pipeline.registerStep(step)); return pipeline; } /** * Validate a pipeline configuration * * @param config - Pipeline configuration to validate * @returns Array of validation errors (empty if valid) */ export function validatePipelineConfig(config) { const errors = []; if (!config.steps || config.steps.length === 0) { errors.push('Pipeline configuration must include at least one step'); } // Check for duplicate step names const stepNames = new Set(); for (const step of config.steps || []) { if (stepNames.has(step.name)) { errors.push(`Duplicate step name: ${step.name}`); } stepNames.add(step.name); // Check for valid processor if (!step.processor || typeof step.processor.process !== 'function') { errors.push(`Step '${step.name}' has invalid processor`); } // Check for valid order if (typeof step.order !== 'number' || step.order < 0) { errors.push(`Step '${step.name}' has invalid order: ${step.order}`); } } // Check for valid field tracking mode if (config.fieldTrackingMode && !['centralized', 'distributed', 'disabled'].includes(config.fieldTrackingMode)) { errors.push(`Invalid field tracking mode: ${config.fieldTrackingMode}`); } return errors; } /** * Get processor instance by name for testing or custom configurations * * @param processorName - Name of the processor to get * @returns Processor instance or undefined if not found */ export function getProcessorByName(processorName) { const processors = { 'rst-conversion': () => new RstProcessor(), 'latex-conversion': () => new LatexProcessor(), 'yaml-parsing': () => new YamlProcessor(), imports: () => new ImportsProcessor(), clauses: () => new ClausesProcessor(), references: () => new ReferencesProcessor(), mixins: () => new MixinsProcessor(), 'template-loops': () => new TemplateLoopsProcessor(), headers: () => new HeadersProcessor(), 'metadata-export': () => new MetadataExportProcessor(), 'field-tracking': () => new FieldTrackingProcessor(), }; const factory = processors[processorName]; return factory ? factory() : undefined; } //# sourceMappingURL=pipeline-config.js.map