legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
236 lines • 11 kB
JavaScript
/**
* Service integration for Interactive CLI
*
* This module provides the bridge between the interactive CLI configuration
* and the existing CliService, translating user selections into appropriate
* CLI options and orchestrating the document processing workflow.
*/
import * as path from 'path';
import { RESOLVED_PATHS } from '../../constants/index.js';
import { ArchiveManager } from '../../utils/archive-manager.js';
import { readFileSync } from '../../utils/index.js';
import { buildProcessingContext, processLegalMarkdown, generateAllFormats, buildFormatGenerationOptions, } from '../../core/pipeline/index.js';
/**
* Get the path to the highlight CSS file from the package
* Works in both CommonJS and ESM environments
*/
function getHighlightCssPath() {
const isEsm = typeof __filename === 'undefined' || typeof __dirname === 'undefined';
if (!isEsm && typeof __dirname !== 'undefined') {
return path.resolve(__dirname, '..', '..', 'styles', 'highlight.css');
}
// Fallback for ESM environments
return path.join(process.cwd(), 'src', 'styles', 'highlight.css');
}
/**
* Interactive service for processing documents with user-selected configuration
*
* This class serves as an adapter between the interactive CLI configuration
* collected from user prompts and the existing CliService infrastructure,
* ensuring seamless integration with the established processing pipeline.
*/
export class InteractiveService {
config;
/**
* Initialize the interactive service with user configuration
*
* @param config Complete interactive configuration collected from user prompts
*/
constructor(config) {
this.config = config;
}
/**
* Map interactive configuration to CLI service options
*
* Translates the structured interactive configuration into the format
* expected by the existing CliService, ensuring all user selections
* are properly represented in the processing options.
*
* @param config Interactive configuration to convert
* @returns CLI service options with all necessary flags and paths configured
*/
mapToCliOptions(config) {
const { outputFormats, processingOptions, archiveOptions, cssFile } = config;
return {
debug: processingOptions.debug,
yamlOnly: false,
noHeaders: false,
noClauses: false,
noReferences: false,
noImports: false,
noMixins: false,
noReset: false,
noIndent: false,
throwOnYamlError: false,
toMarkdown: outputFormats.markdown,
exportMetadata: outputFormats.metadata,
exportFormat: 'yaml',
basePath: RESOLVED_PATHS.DEFAULT_INPUT_DIR,
verbose: processingOptions.debug,
pdf: outputFormats.pdf,
html: outputFormats.html,
docx: outputFormats.docx,
highlight: processingOptions.highlight,
enableFieldTrackingInMarkdown: processingOptions.fieldTracking,
css: cssFile ? path.join(RESOLVED_PATHS.STYLES_DIR, cssFile) : undefined,
title: config.outputFilename,
archiveSource: archiveOptions.enabled ? archiveOptions.directory || true : false,
format: undefined, // Interactive CLI doesn't collect this, use default
landscape: undefined, // Interactive CLI doesn't collect this, use default
};
}
/**
* Process the input file and generate all requested outputs
*
* Coordinates the processing of the selected input file according to the
* user's configuration, generating output files in all requested formats
* and writing them to the configured output directory.
*
* @param inputFile Absolute path to the input document to process
* @returns Promise resolving to processing result with output files and archive info
* @throws Error when processing fails for any requested format
*/
async processFile(inputFile) {
const outputFiles = [];
const { outputFilename, outputFormats, archiveOptions } = this.getConfig();
try {
// Determine the output directory: if archiving is enabled, use archive directory
let outputDir;
if (archiveOptions.enabled && archiveOptions.directory) {
// Custom directory should be relative to DEFAULT_OUTPUT_DIR
const customDir = archiveOptions.directory.replace(/\/+$/, '');
outputDir = path.resolve(RESOLVED_PATHS.DEFAULT_OUTPUT_DIR, customDir);
}
else {
outputDir = RESOLVED_PATHS.DEFAULT_OUTPUT_DIR;
}
// Read file content
const content = readFileSync(inputFile);
const inputDir = path.dirname(inputFile);
// Map interactive config to processing options
const processingOptions = this.mapToCliOptions(this.getConfig());
// PHASE 1: Build processing context (parses YAML, resolves force-commands)
const context = await buildProcessingContext(content, processingOptions, inputDir);
// PHASE 2: Process content ONCE (runs remark pipeline, caches AST)
const processedResult = await processLegalMarkdown(context.content, {
...context.options,
additionalMetadata: context.metadata, // Pass YAML metadata for header processing
});
// PHASE 3: Generate all formats from cached result (NO re-processing!)
const highlightCssPath = getHighlightCssPath();
const cssPath = context.options.cssPath ?? processingOptions.css;
const formatGenerationOptions = buildFormatGenerationOptions(context.options, {
outputDir,
baseFilename: outputFilename,
pdf: outputFormats.pdf,
html: outputFormats.html,
docx: outputFormats.docx,
markdown: outputFormats.markdown && !archiveOptions.enabled, // Skip if archiving
metadata: outputFormats.metadata,
highlight: processingOptions.highlight,
cssPath,
highlightCssPath,
title: outputFilename,
format: processingOptions.format,
landscape: processingOptions.landscape,
exportFormat: 'yaml',
exportPath: path.join(outputDir, `${outputFilename}-metadata.yaml`),
});
const formatResult = await generateAllFormats(processedResult, formatGenerationOptions);
// Add all generated files to output
outputFiles.push(...formatResult.generatedFiles);
// Handle archiving separately after all processing is complete
let archiveResult;
if (archiveOptions.enabled) {
// Use the already-processed content from Phase 2
archiveResult = await this.handleArchivingWithProcessedContent(inputFile, content, processedResult.content);
// Include archived MD files in outputFiles so they appear in the MD section
if (archiveResult && archiveResult.success) {
if (archiveResult.contentsIdentical && archiveResult.archivedPath) {
// Single archived file (content identical)
if (archiveResult.archivedPath.endsWith('.md')) {
outputFiles.push(archiveResult.archivedPath);
}
}
else {
// Two archived files (content different) - include both MD files
if (archiveResult.archivedOriginalPath &&
archiveResult.archivedOriginalPath.endsWith('.md')) {
outputFiles.push(archiveResult.archivedOriginalPath);
}
if (archiveResult.archivedProcessedPath &&
archiveResult.archivedProcessedPath.endsWith('.md')) {
outputFiles.push(archiveResult.archivedProcessedPath);
}
}
}
}
return {
outputFiles,
archiveResult,
};
}
catch (error) {
throw new Error(`Processing failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Handle archiving of source file using already-processed content
*
* This method reuses the processed content from Phase 2 instead of
* re-processing the file, eliminating duplicate processing.
*
* @param inputFile Path to the source file to archive
* @param originalContent Original file content
* @param processedContent Already-processed content from Phase 2
* @returns Archive operation result information
*/
async handleArchivingWithProcessedContent(inputFile, originalContent, processedContent) {
try {
const archiveManager = new ArchiveManager();
const { archiveOptions } = this.getConfig();
// Determine archive directory - should be relative to project root
let archiveDir;
if (archiveOptions.directory) {
// Custom directory should be relative to project root (consistent with ARCHIVE_DIR)
// Ensure directory name doesn't have trailing slash (normalize)
const customDir = archiveOptions.directory.replace(/\/+$/, '');
archiveDir = path.resolve(process.cwd(), customDir);
}
else {
archiveDir = RESOLVED_PATHS.ARCHIVE_DIR;
}
// Use smart archiving with already-processed content (NO re-processing!)
const result = await archiveManager.smartArchiveFile(inputFile, {
archiveDir,
createDirectory: true,
conflictResolution: 'rename',
originalContent,
processedContent, // Reuse from Phase 2
});
return {
success: result.success,
contentsIdentical: result.contentsIdentical,
archivedPath: result.archivedPath,
archivedOriginalPath: result.archivedOriginalPath,
archivedProcessedPath: result.archivedProcessedPath,
error: result.error,
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Get the current configuration
*
* @returns The interactive configuration used to initialize this service
*/
getConfig() {
return this.config;
}
}
//# sourceMappingURL=service.js.map