UNPKG

legal-markdown-js

Version:

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

761 lines 34.2 kB
/** * Legal Markdown Processor with Remark Pipeline (Phase 3: AST Processing) * * This module provides a comprehensive processor for Legal Markdown documents * using the remark ecosystem. It combines multiple remark plugins to provide * AST-based processing that avoids text contamination issues. * * Processing Flow: * - Phase 1: Context building (YAML parsing, metadata merging) - done in context-builder.ts * - Phase 2: String transformations (field normalization, clauses, loops) - done in string-transformations.ts * - Phase 3: AST processing (THIS MODULE - remark plugins) * - Phase 4: Format generation (HTML, PDF) - done in format-generator.ts * * Features: * - Complete remark pipeline with all Legal Markdown plugins * - Field tracking integration with automatic clearing * - Cross-reference processing with section numbering * - Metadata extraction and processing * - Comprehensive error handling and debugging * - Compatible with existing Legal Markdown API * * @example * ```typescript * import { processLegalMarkdownWithRemark } from './legal-markdown-processor.js'; * * const result = await processLegalMarkdownWithRemark(content, { * basePath: './documents', * enableFieldTracking: true, * debug: true * }); * * console.log(result.content); // Processed markdown * console.log(result.metadata); // Extracted metadata * console.log(result.fieldReport); // Field tracking report * ``` * * @see docs/architecture/03_processing_pipeline.md * @see docs/architecture/string-transformations.md * @module */ import { unified } from 'unified'; import remarkParse from 'remark-parse'; import remarkGfm from 'remark-gfm'; import remarkStringify from 'remark-stringify'; import { remarkCrossReferences, remarkTemplateFields, remarkHeaders, remarkMixins, remarkImports, remarkLegalHeadersParser, remarkDates, remarkSignatureLines, } from '../../plugins/remark/index.js'; import { PluginOrderValidator } from '../../plugins/remark/plugin-order-validator.js'; import { GLOBAL_PLUGIN_REGISTRY } from '../../plugins/remark/plugin-metadata-registry.js'; import { fieldTracker, FieldStatus } from '../tracking/field-tracker.js'; import { flattenObject } from '../../core/utils/object-flattener.js'; import { parseYamlFrontMatter } from '../../core/parsers/yaml-parser.js'; import { exportMetadata } from '../../core/exporters/metadata-exporter.js'; import { asMarkdown } from '../../types/content-formats.js'; import { applyStringTransformations } from '../../core/pipeline/string-transformations.js'; import { detectSyntaxType } from '../template-loops.js'; import { parseForceCommands, applyForceCommands } from '../../core/parsers/force-commands-parser.js'; import { logger } from '../../utils/logger.js'; /** * Unsafe patterns for markdown serialization, excluding underscore patterns. * * Based on mdast-util-to-markdown@2.1.2 default unsafe patterns, with * underscore patterns removed since we use asterisks for emphasis/strong. * * Why hardcoded instead of imported: * - The lib/unsafe.js file is not exported in package.json exports * - Dynamic import would fail in ESM environments * - These patterns are stable across mdast-util-to-markdown versions * * According to CommonMark spec 0.31.2, underscores between alphanumeric * characters or between other underscores cannot create emphasis (they are * both left-flanking AND right-flanking), so they don't need to be escaped. * * Source: https://github.com/syntax-tree/mdast-util-to-markdown/blob/2.1.2/lib/unsafe.js * Last reviewed: 2025-01-02 (mdast-util-to-markdown@2.1.2) * * Excluded patterns: * - {atBreak: true, character: '_'} * - {character: '_', inConstruct: 'phrasing', notInConstruct: ['autolink', 'destinationLiteral', ...]} */ const unsafeWithoutUnderscores = [ { character: '\t', after: '[\\r\\n]', inConstruct: 'phrasing' }, { character: '\t', before: '[\\r\\n]', inConstruct: 'phrasing' }, { character: '\t', inConstruct: ['codeFencedLangGraveAccent', 'codeFencedLangTilde'] }, { character: '\r', inConstruct: [ 'codeFencedLangGraveAccent', 'codeFencedLangTilde', 'codeFencedMetaGraveAccent', 'codeFencedMetaTilde', 'destinationLiteral', 'headingAtx', ], }, { character: '\n', inConstruct: [ 'codeFencedLangGraveAccent', 'codeFencedLangTilde', 'codeFencedMetaGraveAccent', 'codeFencedMetaTilde', 'destinationLiteral', 'headingAtx', ], }, { character: ' ', after: '[\\r\\n]', inConstruct: 'phrasing' }, { character: ' ', before: '[\\r\\n]', inConstruct: 'phrasing' }, { character: ' ', inConstruct: ['codeFencedLangGraveAccent', 'codeFencedLangTilde'] }, { character: '!', after: '\\[', inConstruct: 'phrasing', notInConstruct: [ 'autolink', 'destinationLiteral', 'destinationRaw', 'reference', 'titleQuote', 'titleApostrophe', ], }, { character: '"', inConstruct: 'titleQuote' }, { atBreak: true, character: '#' }, { character: '#', inConstruct: 'headingAtx', after: '(?:[\r\n]|$)' }, { character: '&', after: '[#A-Za-z]', inConstruct: 'phrasing' }, { character: "'", inConstruct: 'titleApostrophe' }, { character: '(', inConstruct: 'destinationRaw' }, { before: '\\]', character: '(', inConstruct: 'phrasing', notInConstruct: [ 'autolink', 'destinationLiteral', 'destinationRaw', 'reference', 'titleQuote', 'titleApostrophe', ], }, { atBreak: true, before: '\\d+', character: ')' }, { character: ')', inConstruct: 'destinationRaw' }, { atBreak: true, character: '*', after: '(?:[ \t\r\n*])' }, { character: '*', inConstruct: 'phrasing', notInConstruct: [ 'autolink', 'destinationLiteral', 'destinationRaw', 'reference', 'titleQuote', 'titleApostrophe', ], }, { atBreak: true, character: '+', after: '(?:[ \t\r\n])' }, { atBreak: true, character: '-', after: '(?:[ \t\r\n-])' }, { atBreak: true, before: '\\d+', character: '.', after: '(?:[ \t\r\n]|$)' }, { atBreak: true, character: '<', after: '[!/?A-Za-z]' }, { character: '<', after: '[!/?A-Za-z]', inConstruct: 'phrasing', notInConstruct: [ 'autolink', 'destinationLiteral', 'destinationRaw', 'reference', 'titleQuote', 'titleApostrophe', ], }, { character: '<', inConstruct: 'destinationLiteral' }, { atBreak: true, character: '=' }, { atBreak: true, character: '>' }, { character: '>', inConstruct: 'destinationLiteral' }, { atBreak: true, character: '[' }, { character: '[', inConstruct: 'phrasing', notInConstruct: [ 'autolink', 'destinationLiteral', 'destinationRaw', 'reference', 'titleQuote', 'titleApostrophe', ], }, { character: '[', inConstruct: ['label', 'reference'] }, { character: '\\', after: '[\\r\\n]', inConstruct: 'phrasing' }, { character: ']', inConstruct: ['label', 'reference'] }, // NOTE: Underscore patterns are intentionally excluded here // Original patterns were: // {atBreak: true, character: '_'}, // {character: '_', inConstruct: 'phrasing', notInConstruct: fullPhrasingSpans}, { atBreak: true, character: '`' }, { character: '`', inConstruct: ['codeFencedLangGraveAccent', 'codeFencedMetaGraveAccent'] }, { character: '`', inConstruct: 'phrasing', notInConstruct: [ 'autolink', 'destinationLiteral', 'destinationRaw', 'reference', 'titleQuote', 'titleApostrophe', ], }, { atBreak: true, character: '~' }, ]; /** * Pre-process content to escape underscores inside {{}} to prevent * markdown parser from interpreting them as italic delimiters * * **Problem**: Field names with underscores like `{{counterparty.legal_name}}` * would be parsed by remark as `{{counterparty.legal*name}}` because markdown * treats `_text_` as emphasis (converted to `*text*` during parsing). * * **Solution**: Escape underscores to `\_` before markdown parsing. The escaped * underscores are later unescaped in: * - `remarkTemplateFields` (src/plugins/remark/template-fields.ts) * - `parseMarkdownInlineFormatting` (src/plugins/remark/legal-headers-parser.ts) * * @see https://github.com/petalo/legal-markdown-js/issues/139 * @see src/plugins/remark/template-fields.ts - Unescapes underscores during field extraction * @see src/plugins/remark/legal-headers-parser.ts - Excludes template fields from emphasis parsing * * @param content - Raw markdown content * @returns Content with underscores escaped for legacy syntax, unchanged for Handlebars * * @example * ```typescript * // Legacy syntax - escapes underscores * escapeTemplateUnderscores('{{legal_name}}') // => '{{legal\_name}}' * * // Handlebars syntax - no escaping needed * escapeTemplateUnderscores('{{titleCase section_name}}') // => '{{titleCase section_name}}' * escapeTemplateUnderscores('{{#if test}}') // => '{{#if test}}' (unchanged) * ``` */ function escapeTemplateUnderscores(content) { const syntaxType = detectSyntaxType(content); if (syntaxType === 'handlebars' || syntaxType === 'mixed') { return content; } return content.replace(/\{\{([^}]+)\}\}/g, (match, inner) => { const trimmed = inner.trim(); // Don't escape loop/conditional markers - these are processed by processTemplateLoops // and should not have their syntax modified if (trimmed.startsWith('#') || // {{#if}}, {{#each}}, etc. trimmed.startsWith('/') || // {{/if}}, {{/each}}, etc. trimmed === 'else' // {{else}} ) { return match; } // Escape underscores to prevent markdown parser from interpreting them as // italic delimiters. These will be unescaped during template field expansion. const escaped = inner.replace(/_/g, '\\_'); return `{{${escaped}}}`; }); } function resolveTrackingOptions(options) { const trackingEnabled = options.enableFieldTracking ?? false; const explicitAst = options.astFieldTracking; const explicitLogic = options.logicBranchHighlighting; const astFieldTracking = explicitAst ?? (explicitLogic === true ? true : trackingEnabled); const logicBranchHighlighting = explicitLogic ?? astFieldTracking; return { astFieldTracking, logicBranchHighlighting, }; } /** * Create a configured remark processor for Legal Markdown * * This function assembles a unified processor with all the necessary remark plugins * for Legal Markdown processing. Plugins are added in a specific order to ensure * proper processing dependencies: * * **CRITICAL PLUGIN ORDER:** * 1. Imports - MUST be first to load all content before any transformation * 2. Legal Headers Parser - MUST be after imports to convert headers in imported files * 3. Mixins - Content expansion before other processing * 4. Clauses - Conditional content * 5. Template fields - Field processing and tracking * 6. Cross-references - Reference resolution * 7. Headers - Final structure processing and numbering * * **WARNING:** Changing this order can break functionality: * - If Legal Headers Parser runs before Imports, headers in imported files won't be converted * - If Cross-references runs before Headers, section numbering won't be available * - If Mixins runs before Imports, variable expansion won't work for imported content * * @param metadata - Document metadata from YAML frontmatter and additional sources * @param options - Configuration options for processing * @returns Configured unified processor ready for content processing * @internal */ function createLegalMarkdownProcessor(metadata, options) { const trackingOptions = resolveTrackingOptions(options); const processor = unified().use(remarkParse).use(remarkGfm); // Track plugin names for order validation const pluginOrder = []; // Step 1: Add imports plugin FIRST - must load all content before any transformation // This ensures that imported files are loaded before any other processing if (!options.noImports) { processor.use(remarkImports, { basePath: options.basePath || '.', mergeMetadata: true, debug: options.debug, maxDepth: 10, timeoutMs: 30000, filterReserved: true, validateTypes: options.validateImportTypes ?? true, logImportOperations: options.logImportOperations ?? options.debug, importTracing: options.importTracing, }); } // Step 2: Add legal headers parser AFTER imports to convert l., ll., etc. to proper headers // This ensures headers in imported files are also converted // WARNING: Do not move this before imports or headers in imported files won't be processed // NOTE: Optional clauses are processed in Phase 2 (string transformations) before AST parsing if (!options.noHeaders) { processor.use(remarkLegalHeadersParser, { debug: options.debug, }); } // Add mixins plugin (should be processed early for content expansion) if (!options.noMixins) { processor.use(remarkMixins, { metadata, basePath: options.basePath || '.', debug: options.debug, maxDepth: 5, }); } // Skip loop processing plugins since we pre-process loops // Consolidation and loop processing now happen before remark parsing // Add dates plugin (processes @today tokens with arithmetic and formatting) processor.use(remarkDates, { metadata, debug: options.debug, enableFieldTracking: options.enableFieldTracking, }); // Add signature lines plugin (wraps long underscore sequences with CSS class) processor.use(remarkSignatureLines, { minUnderscores: 10, addCssClass: true, cssClassName: 'signature-line', debug: options.debug, }); // Add template fields plugin (processes {{field}} patterns) processor.use(remarkTemplateFields, { metadata, fieldPatterns: options.fieldPatterns, enableFieldTracking: options.enableFieldTracking, astFieldTracking: trackingOptions.astFieldTracking, logicBranchHighlighting: trackingOptions.logicBranchHighlighting, debug: options.debug, }); // Add cross-references plugin BEFORE headers (needs to extract |key| patterns before headers removes them) if (!options.disableCrossReferences && !options.noReferences) { processor.use(remarkCrossReferences, { metadata, enableFieldTracking: options.enableFieldTracking, debug: options.debug, }); } // Add headers plugin AFTER cross-references (processes headers after cross-reference extraction) if (!options.noHeaders) { processor.use(remarkHeaders, { metadata, noReset: options.noReset, noIndent: options.noIndent, debug: options.debug, }); } // Add stringify plugin with proper markdown formatting processor.use(remarkStringify, { emphasis: '*', // Use * for _italic_ strong: '*', // Use ** for __bold__ (asterisks, not underscores) bullet: '-', // Use - for bullets fence: '`', // Use ` for code blocks fences: true, // Use fenced code blocks incrementListMarker: true, // Increment list markers (1. 2. 3.) rule: '-', // Use --- for horizontal rules (not ***) ruleRepetition: 3, // Use exactly 3 dashes for horizontal rules unsafe: unsafeWithoutUnderscores, // Filtered unsafe patterns (no underscore rules) handlers: { // Override text node serialization to prevent underscore escaping. // mdast-util-to-markdown's `unsafe` option is ADDITIVE - it merges with // hardcoded defaults that include underscore escaping rules. There is no // public API to REMOVE default unsafe patterns. Since we configure // emphasis:'*' and strong:'*', underscores in output text are never // markdown syntax - escaped \_ is always a serialization artifact. // See: https://github.com/syntax-tree/mdast-util-to-markdown/blob/2.1.2/lib/unsafe.js // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mdast-util-to-markdown handler parameters are not publicly typed text(node, _parent, state, info) { const result = state.safe(node.value, { before: info.before, after: info.after, encode: info.encode, }); // Strip backslash escapes that are not valid markdown syntax: // - \_ : underscores never form emphasis when using emphasis:'*' / strong:'*' // - \@ : email @-signs are plain text in legal docs (not GFM @mentions) return result.replaceAll('\\_', '_').replaceAll('\\@', '@'); }, }, }); // Build plugin order list for validation (must match actual processor.use() order above) // NOTE: Optional clauses (remarkClauses) are now processed in Phase 2 (string transformations) if (!options.noImports) pluginOrder.push('remarkImports'); if (!options.noHeaders) pluginOrder.push('remarkLegalHeadersParser'); if (!options.noMixins) pluginOrder.push('remarkMixins'); pluginOrder.push('remarkDates'); pluginOrder.push('remarkSignatureLines'); pluginOrder.push('remarkTemplateFields'); if (!options.disableCrossReferences && !options.noReferences) { pluginOrder.push('remarkCrossReferences'); } if (!options.noHeaders) pluginOrder.push('remarkHeaders'); // Validate plugin order if (options.debug || options.validatePluginOrder) { try { const validator = new PluginOrderValidator(GLOBAL_PLUGIN_REGISTRY); const result = validator.validate(pluginOrder, { throwOnError: false, // Don't throw, just log warnings logWarnings: options.debug || options.validatePluginOrder, strictMode: false, debug: options.debug, }); if (!result.valid && options.debug) { logger.warn('Plugin order validation warnings detected'); for (const error of result.errors) { logger.warn(` - ${error.message}`); } if (result.suggestedOrder) { logger.warn('Suggested order', result.suggestedOrder.join(' → ')); } } } catch (error) { // If validation fails, just log a warning in debug mode if (options.debug) { logger.warn('Plugin order validation failed', error instanceof Error ? error.message : String(error)); } } } return processor; } /** * Process Legal Markdown content using remark pipeline * * This is the main entry point for remark-based Legal Markdown processing. * It provides a complete AST-based processing pipeline with: * - YAML frontmatter parsing and metadata extraction * - Template field processing with nested helper support * - Cross-reference resolution and section numbering * - Field tracking for document highlighting * - Import processing for modular documents * - Comprehensive error handling and debugging * * @param content - Raw Legal Markdown content to process * @param options - Processing options and configuration * @returns Promise resolving to processed content, metadata, and statistics * * @example * ```typescript * const result = await processLegalMarkdownWithRemark(markdownContent, { * enableFieldTracking: true, * basePath: './templates', * debug: true * }); * * console.log(result.content); // Processed markdown * console.log(result.stats.fieldsTracked); // Number of tracked fields * ``` */ export async function processLegalMarkdown(content, options = {}) { const startTime = Date.now(); const warnings = []; const pluginsUsed = ['remarkParse', 'remarkStringify']; try { // Clear field tracker at the beginning fieldTracker.clear(); if (options.debug) { logger.debug('Starting Legal Markdown processing with remark pipeline'); } // Parse YAML front matter first if (options.debug) { logger.debug('Raw content length', content.length); logger.debug('Raw content first 500 chars', content.substring(0, 500)); } const { content: contentWithoutYaml, metadata: yamlMetadata } = parseYamlFrontMatter(content, options.throwOnYamlError); if (options.debug) { logger.debug('YAML parsed', Object.keys(yamlMetadata)); logger.debug('Sample metadata', yamlMetadata); } // Pre-process: Escape underscores inside {{}} to prevent markdown italic parsing // This prevents {{_field}} from being parsed as {{*field}} const contentWithEscapedTemplates = escapeTemplateUnderscores(contentWithoutYaml); if (options.debug && contentWithEscapedTemplates !== contentWithoutYaml) { logger.debug('Escaped template underscores'); } // If key processing is disabled, return original content without remark processing // Check if the main content-altering features are disabled const keyProcessingDisabled = options.noHeaders && options.noReferences && options.noMixins; if (options.debug) { logger.debug('Processing flags', { noHeaders: options.noHeaders, noReferences: options.noReferences, noMixins: options.noMixins, enableFieldTracking: options.enableFieldTracking, keyProcessingDisabled, }); } if (keyProcessingDisabled && !options.enableFieldTracking) { if (options.debug) { logger.debug('Key processing disabled, returning original content'); } return { content: asMarkdown(contentWithEscapedTemplates), metadata: { ...yamlMetadata, ...options.additionalMetadata }, stats: { processingTime: Date.now() - startTime, pluginsUsed: [], crossReferencesFound: 0, fieldsTracked: 0, }, warnings: [], }; } // If only processing YAML, return early if (options.yamlOnly) { return { content: asMarkdown(contentWithEscapedTemplates), metadata: yamlMetadata, stats: { processingTime: Date.now() - startTime, pluginsUsed: [], crossReferencesFound: 0, fieldsTracked: 0, }, warnings: [], }; } // Merge metadata sources const combinedMetadata = { ...yamlMetadata, ...options.additionalMetadata, }; // Process force commands and metadata options let updatedOptions = { ...options }; // Parse force_commands from metadata if present (higher precedence) if (combinedMetadata.force_commands && typeof combinedMetadata.force_commands === 'string') { const forceCommands = parseForceCommands(combinedMetadata.force_commands, combinedMetadata); if (forceCommands) { updatedOptions = applyForceCommands(updatedOptions, forceCommands); } } if (updatedOptions.debug) { logger.debug('Combined metadata keys', Object.keys(combinedMetadata)); logger.debug('Combined metadata sample', combinedMetadata); logger.debug('Services structure', JSON.stringify(combinedMetadata.services, null, 2)); logger.debug('Milestones structure', JSON.stringify(combinedMetadata.milestones, null, 2)); } const trackingOptions = resolveTrackingOptions(updatedOptions); // ────────────────────────────────────────────────────────────────────────── // Phase 2: String Transformations // ────────────────────────────────────────────────────────────────────────── // Apply all string-level transformations BEFORE remark AST parsing // This includes: field normalization, optional clauses, template loops // See: docs/architecture/string-transformations.md if (updatedOptions.debug) { logger.debug('Starting Phase 2: String Transformations'); } const stringTransformResult = await applyStringTransformations(contentWithEscapedTemplates, { metadata: combinedMetadata, debug: updatedOptions.debug, enableFieldTracking: updatedOptions.enableFieldTracking, astFieldTracking: trackingOptions.astFieldTracking, logicBranchHighlighting: trackingOptions.logicBranchHighlighting, noClauses: updatedOptions.noClauses, fieldPatterns: updatedOptions.fieldPatterns, }); const preprocessedContent = stringTransformResult.content; const escapedTemplates = stringTransformResult.escapedTemplates; // Merge updated metadata from string transformations (includes field mappings) Object.assign(combinedMetadata, stringTransformResult.metadata); if (updatedOptions.debug) { logger.debug('Phase 2 complete. Content length', preprocessedContent.length); if (preprocessedContent.includes('{{#')) { logger.warn('Content still has {{# patterns after Phase 2'); } } // Ensure default pattern is always included for template field processing // since preprocessing normalizes all patterns to {{field}} format const processingOptions = { ...updatedOptions, astFieldTracking: trackingOptions.astFieldTracking, logicBranchHighlighting: trackingOptions.logicBranchHighlighting, fieldPatterns: updatedOptions.fieldPatterns && updatedOptions.fieldPatterns.length > 0 ? ['{{(.+?)}}', ...updatedOptions.fieldPatterns] : undefined, }; // Create and configure processor const processor = createLegalMarkdownProcessor(combinedMetadata, processingOptions); // Track which plugins are being used pluginsUsed.push('remarkTemplateFields'); if (!updatedOptions.disableCrossReferences && !updatedOptions.noReferences) { pluginsUsed.push('remarkCrossReferences'); } if (updatedOptions.debug) { logger.debug(`Using plugins: ${pluginsUsed.join(', ')}`); } // Process the content (field highlighting is now done during AST processing) const result = await processor.process(preprocessedContent); let processedContent = String(result); // Restore escaped template literals: __LMESC_N__ → {{...}} // These were collected from \{{...}} patterns before Phase 2 string transformations. if (escapedTemplates.length > 0) { for (let i = 0; i < escapedTemplates.length; i++) { processedContent = processedContent.replace(new RegExp(`__LMESC_${i}__`, 'g'), escapedTemplates[i]); } } // Cache the AST for Phase 3 format generation // The AST is available in result.data (after processing) const processedTree = result.value; // Extract imported metadata from the processed tree const importedMetadata = result.history[0]?._importedMetadata || {}; // Merge imported metadata with combined metadata (imported metadata has lower priority) const finalMetadata = { ...importedMetadata, ...combinedMetadata, }; if (updatedOptions.debug && Object.keys(importedMetadata).length > 0) { logger.debug(`Merged ${Object.keys(importedMetadata).length} imported metadata fields`, Object.keys(importedMetadata)); } // Extract processing statistics const crossRefsValue = combinedMetadata['_cross_references']; const crossReferencesFound = Array.isArray(crossRefsValue) ? crossRefsValue.length : 0; const fieldsTracked = updatedOptions.enableFieldTracking ? fieldTracker.getTotalOccurrences() : 0; // Generate field report if field tracking is enabled let fieldReport; if (updatedOptions.enableFieldTracking) { const fields = fieldTracker.getFields(); // Add YAML keys that were declared but never used as template fields const flatMeta = flattenObject(finalMetadata); for (const [key, value] of Object.entries(flatMeta)) { if (!key.startsWith('_') && !fields.has(key)) { fields.set(key, { name: key, status: FieldStatus.DECLARED, value: value, hasLogic: false, }); } } fieldReport = { totalFields: fieldsTracked, uniqueFields: fields.size, fields: fields, }; } // Handle metadata export if requested let exportedFiles = []; if (updatedOptions.exportMetadata) { try { const exportResult = exportMetadata(finalMetadata, updatedOptions.exportFormat, updatedOptions.exportPath); exportedFiles = exportResult.exportedFiles; if (updatedOptions.debug) { logger.debug(`Exported metadata files: ${exportedFiles.join(', ')}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); warnings.push(`Failed to export metadata: ${errorMessage}`); if (updatedOptions.debug) { logger.warn('Metadata export failed', error instanceof Error ? error.message : String(error)); } } } if (updatedOptions.debug) { logger.debug(`Processing completed in ${Date.now() - startTime}ms`); logger.debug(`Cross-references found: ${crossReferencesFound}`); logger.debug(`Fields tracked: ${fieldsTracked}`); } return { content: asMarkdown(processedContent), metadata: finalMetadata, ast: processedTree, // Cached AST for Phase 3 exportedFiles: exportedFiles.length > 0 ? exportedFiles : undefined, fieldReport, stats: { processingTime: Date.now() - startTime, pluginsUsed, crossReferencesFound, fieldsTracked, }, warnings, }; } catch (error) { if (options.debug) { logger.error('Legal Markdown processing failed', error instanceof Error ? error.message : String(error)); } // Re-throw with additional context const enhancedError = new Error(`Legal Markdown processing failed: ${error instanceof Error ? error.message : String(error)}`); // Store the original error for debugging (avoiding .cause for compatibility) enhancedError.originalError = error; throw enhancedError; } } /** * Create a pre-configured processor instance for reuse * * This factory function creates a reusable processor instance with pre-configured * options. Useful for batch processing multiple documents with the same settings. * * @param options - Base configuration options for all processing operations * @returns Object with async and sync processing methods * * @example * ```typescript * const processor = createReusableLegalMarkdownProcessor({ * enableFieldTracking: true, * basePath: './templates' * }); * * const result1 = await processor.process(content1); * const result2 = await processor.process(content2, { debug: true }); * ``` */ export function createReusableLegalMarkdownProcessor(options = {}) { return { async process(content, additionalOptions = {}) { return processLegalMarkdown(content, { ...options, ...additionalOptions, }); }, processSync(content, additionalOptions = {}) { void content; void additionalOptions; throw new Error('Synchronous processing is not available with the remark-based processor. ' + 'Please use processLegalMarkdown() (async) instead.'); }, }; } // Types are already exported inline, no need for separate export // Exported for testing - not part of public API export { escapeTemplateUnderscores as _escapeTemplateUnderscores, createLegalMarkdownProcessor as _createLegalMarkdownProcessor, }; //# sourceMappingURL=legal-markdown-processor.js.map