legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
257 lines • 11.9 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 { CliService } from '../service.js';
import { RESOLVED_PATHS } from '../../constants/index.js';
import { ArchiveManager } from '../../utils/archive-manager.js';
import { readFileSync } from '../../utils/index.js';
import { processLegalMarkdown } from '../../index.js';
/**
* 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,
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,
};
}
/**
* 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;
}
// Create a CliService without archiving for individual format processing
const processingOptions = this.mapToCliOptions(this.getConfig());
const nonArchivingService = new CliService({
...processingOptions,
archiveSource: false, // Disable archiving during format processing
exportMetadata: false, // Disable metadata export to prevent duplications
silent: true, // Suppress success messages to prevent duplication
});
// Process for each selected format
if (outputFormats.pdf) {
const pdfOutput = path.join(outputDir, `${outputFilename}.pdf`);
await nonArchivingService.processFile(inputFile, pdfOutput);
outputFiles.push(pdfOutput);
// Add highlight version if highlight is enabled
if (processingOptions.highlight) {
const highlightPdfOutput = path.join(outputDir, `${outputFilename}.HIGHLIGHT.pdf`);
outputFiles.push(highlightPdfOutput);
}
}
if (outputFormats.html) {
const htmlOutput = path.join(outputDir, `${outputFilename}.html`);
await nonArchivingService.processFile(inputFile, htmlOutput);
outputFiles.push(htmlOutput);
// Add highlight version if highlight is enabled
if (processingOptions.highlight) {
const highlightHtmlOutput = path.join(outputDir, `${outputFilename}.HIGHLIGHT.html`);
outputFiles.push(highlightHtmlOutput);
}
}
if (outputFormats.markdown) {
// Only generate normal MD file if archiving is disabled
// When archiving is enabled, we'll have .ORIGINAL and .PROCESSED files instead
if (!archiveOptions.enabled) {
const mdOutput = path.join(outputDir, `${outputFilename}.md`);
await nonArchivingService.processFile(inputFile, mdOutput);
outputFiles.push(mdOutput);
}
}
if (outputFormats.metadata) {
const yamlOutput = path.join(outputDir, `${outputFilename}-metadata.yaml`);
// Process the file to get metadata only, without going through full pipeline
const content = readFileSync(inputFile);
const inputDir = path.dirname(inputFile);
const metadataOptions = {
...processingOptions,
basePath: inputDir,
exportMetadata: true,
exportFormat: 'yaml',
exportPath: yamlOutput,
};
const result = processLegalMarkdown(content, metadataOptions);
// Validate that metadata was actually exported
if (result.exportedFiles && result.exportedFiles.length > 0) {
// Filter out imported files - only include actual exported metadata files
const metadataFiles = result.exportedFiles.filter(file => {
const fileName = path.basename(file);
return fileName.includes('-metadata.') || fileName.includes('metadata.');
});
if (metadataFiles.length > 0) {
outputFiles.push(...metadataFiles);
}
else {
throw new Error(`Failed to export metadata to: ${yamlOutput}`);
}
}
else {
throw new Error(`Failed to export metadata to: ${yamlOutput}`);
}
}
// Handle archiving separately after all processing is complete
let archiveResult;
if (archiveOptions.enabled) {
archiveResult = await this.handleArchiving(inputFile);
// 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 and capture results
*
* @param inputFile Path to the source file to archive
* @returns Archive operation result information
*/
async handleArchiving(inputFile) {
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;
}
// Read original content
const originalContent = readFileSync(inputFile);
// Process the content to get processed version
const inputDir = path.dirname(inputFile);
const processedResult = processLegalMarkdown(originalContent, {
basePath: inputDir,
});
// Use smart archiving
const result = await archiveManager.smartArchiveFile(inputFile, {
archiveDir,
createDirectory: true,
conflictResolution: 'rename',
originalContent,
processedContent: processedResult.content,
});
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