UNPKG

legal-markdown-js

Version:

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

171 lines 6.57 kB
/** * Phase 1: Processing Context Builder * * This module implements Phase 1 of the 3-phase pipeline architecture. * It handles document parsing, force-commands resolution, and creates * a unified processing context for subsequent phases. * * Key responsibilities: * - Parse YAML frontmatter * - Resolve force-commands templates (using remark-based processor) * - Merge CLI options, force-commands, and metadata * - Create ProcessingContext for Phase 2 * * @module core/pipeline/context-builder */ import { parseYamlFrontMatter } from '../parsers/yaml-parser.js'; import { extractForceCommands, parseForceCommands, applyForceCommands, } from '../parsers/force-commands-parser.js'; import { ValidationError } from '../../errors/index.js'; import { logger } from '../../utils/logger.js'; /** * Build a processing context from raw content and options * * This is the entry point for Phase 1. It: * 1. Parses YAML frontmatter to extract metadata * 2. Extracts and resolves force-commands (if present) * 3. Merges all options (CLI + force-commands + defaults) * 4. Creates a unified ProcessingContext for Phase 2 * * @param rawContent - Raw markdown content with optional YAML frontmatter * @param cliOptions - Options from CLI or API call * @param basePath - Base path for file resolution * @returns ProcessingContext ready for Phase 2 processing * * @example * ```typescript * const context = await buildProcessingContext( * fileContent, * { pdf: true, highlight: true }, * '/path/to/input/dir' * ); * // context.options contains merged CLI + force-commands options * // context.metadata contains parsed YAML + additional metadata * ``` */ export async function buildProcessingContext(rawContent, cliOptions = {}, basePath = '.') { const startTime = Date.now(); if (cliOptions.debug) { logger.debug('Phase 1: Building processing context', { basePath, cliOptions: Object.keys(cliOptions), }); } // Step 1: Parse YAML frontmatter const { content, metadata } = parseYamlFrontMatter(rawContent, false); if (cliOptions.debug) { logger.debug('Parsed YAML frontmatter', { metadataKeys: Object.keys(metadata || {}), hasMetadata: !!metadata, }); } // Step 2: Start with CLI options as base let effectiveOptions = { ...cliOptions, basePath, // Enable field tracking if highlight is requested enableFieldTracking: cliOptions.enableFieldTracking || cliOptions.highlight, }; // Step 3: Process force-commands if present if (metadata) { const forceCommandsString = extractForceCommands(metadata); if (forceCommandsString) { if (cliOptions.debug) { logger.debug('Found force commands', { forceCommands: forceCommandsString }); } // Parse force commands (this will use template resolution internally) const parsedForceCommands = parseForceCommands(forceCommandsString, metadata, effectiveOptions); if (parsedForceCommands) { // Apply force commands to effective options effectiveOptions = applyForceCommands(effectiveOptions, parsedForceCommands); if (cliOptions.debug) { logger.debug('Applied force commands', { commands: Object.keys(parsedForceCommands), }); } } } } // Step 3.5: Re-check field tracking after force-commands are applied // If force-commands enabled highlight, make sure field tracking is also enabled if (effectiveOptions.highlight && !effectiveOptions.enableFieldTracking) { effectiveOptions.enableFieldTracking = true; if (cliOptions.debug) { logger.debug('Auto-enabled field tracking due to highlight option'); } } // Step 4: Merge additional metadata if provided const finalMetadata = { ...(metadata || {}), ...(cliOptions.additionalMetadata || {}), }; const processingTime = Date.now() - startTime; if (cliOptions.debug) { logger.debug('Phase 1 complete', { processingTime: `${processingTime}ms`, finalOptions: Object.keys(effectiveOptions), metadataKeys: Object.keys(finalMetadata), }); } return { content, rawContent, metadata: finalMetadata, options: effectiveOptions, basePath, }; } /** * Merge multiple metadata objects with proper handling of nested objects * * This function is used when combining metadata from multiple sources: * - Document YAML frontmatter * - Imported file metadata * - Additional metadata from API/CLI * * @param target - Target metadata object to merge into * @param source - Source metadata to merge from * @returns Merged metadata object * @internal */ export function mergeMetadata(target, source) { const result = { ...target }; for (const [key, value] of Object.entries(source)) { if (value && typeof value === 'object' && !Array.isArray(value)) { // Recursively merge nested objects result[key] = mergeMetadata((result[key] || {}), value); } else { // Direct assignment for primitives and arrays result[key] = value; } } return result; } /** * Validate processing context for Phase 2 * * Ensures that the context has all required fields and valid values. * This helps catch configuration errors early in the pipeline. * * @param context - Processing context to validate * @throws Error if context is invalid * @internal */ export function validateProcessingContext(context) { if (!context) { throw new ValidationError('Processing context is null or undefined', 'context'); } if (typeof context.content !== 'string') { throw new ValidationError('Processing context content must be a string', 'content'); } if (!context.metadata || typeof context.metadata !== 'object') { throw new ValidationError('Processing context metadata must be an object', 'metadata'); } if (!context.options || typeof context.options !== 'object') { throw new ValidationError('Processing context options must be an object', 'options'); } if (typeof context.basePath !== 'string') { throw new ValidationError('Processing context basePath must be a string', 'basePath'); } } //# sourceMappingURL=context-builder.js.map