legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
514 lines • 23.8 kB
JavaScript
/**
* Main Entry Point for Legal Markdown Processing Library
*
* This module provides the primary API for processing Legal Markdown documents
* with support for YAML front matter, cross-references, optional clauses, mixins,
* header processing, and multi-format output generation (HTML, PDF).
*
* Features:
* - YAML front matter parsing and metadata extraction
* - Cross-reference processing and resolution
* - Optional clause conditional rendering
* - Mixin system for reusable content blocks
* - Header numbering and formatting
* - Field tracking for document highlighting
* - HTML and PDF generation with styling
* - RST and LaTeX preprocessing support
* - Metadata export capabilities
*
* @example
* ```typescript
* import { processLegalMarkdown, generateHtml, generatePdf } from 'legal-markdown-js';
*
* // Basic document processing
* const result = processLegalMarkdown(content, {
* enableFieldTracking: true,
* basePath: './documents'
* });
*
* // Generate HTML output
* const html = await generateHtml(content, {
* title: 'Legal Agreement',
* includeHighlighting: true
* });
*
* // Generate PDF output
* const pdf = await generatePdf(content, './output.pdf', {
* format: 'A4',
* includeHighlighting: true
* });
* ```
*/
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 './extensions/ast-mixin-processor.js';
import { processTemplateLoops } from './extensions/template-loops.js';
import { exportMetadata } from './core/exporters/metadata-exporter.js';
import { convertRstToLegalMarkdownSync, convertRstToLegalMarkdown } from './extensions/rst-parser.js';
import { convertLatexToLegalMarkdownSync, convertLatexToLegalMarkdown, } from './extensions/latex-parser.js';
import { fieldTracker } from './extensions/tracking/field-tracker.js';
import { htmlGenerator } from './extensions/generators/html-generator.js';
import { pdfGenerator } from './extensions/generators/pdf-generator.js';
import { createDefaultPipeline, createHtmlPipeline } from './extensions/pipeline/pipeline-config.js';
import path from 'path';
import { getCurrentDir } from './utils/esm-utils.js';
// ESM/CJS compatible directory resolution
const currentDir = getCurrentDir();
/**
* Main function to process a Legal Markdown document (async version)
*
* This function orchestrates the complete processing pipeline for Legal Markdown
* documents, including YAML parsing, content preprocessing, clause processing,
* cross-reference resolution, mixin expansion, header formatting, and metadata export.
*
* @param {string} content - The raw Legal Markdown content to process
* @param {LegalMarkdownOptions} [options={}] - Configuration options for processing
* @returns {Promise<Object>} Processing result containing processed content, metadata, and reports
* @returns {string} returns.content - The processed document content
* @returns {Record<string, any>} [returns.metadata] - Extracted YAML metadata
* @returns {string[]} [returns.exportedFiles] - Array of exported metadata files
* @returns {Object} [returns.fieldReport] - Field tracking report if enabled
* @example
* ```typescript
* const result = await processLegalMarkdownAsync(content, {
* enableFieldTracking: true,
* basePath: './documents',
* noClauses: false,
* noReferences: false
* });
*
* console.log(result.content); // Processed markdown
* console.log(result.metadata); // YAML front matter
* console.log(result.fieldReport); // Field usage report
* ```
*/
export async function processLegalMarkdownAsync(content, options = {}) {
try {
// Use the new pipeline system for processing
const pipeline = createDefaultPipeline();
const metadata = {};
const pipelineOptions = {
legalMarkdownOptions: options,
enableStepProfiling: process.env.NODE_ENV === 'development',
};
const result = await pipeline.execute(content, metadata, pipelineOptions);
if (!result.success) {
console.warn('Pipeline execution had errors, falling back to legacy processing');
return processLegalMarkdownLegacy(content, options);
}
// Convert pipeline result to expected format
return {
content: result.content,
metadata: result.metadata,
exportedFiles: result.exportedFiles || [],
fieldReport: result.fieldReport,
};
}
catch (error) {
console.warn('Pipeline system failed, falling back to legacy processing:', error);
return processLegalMarkdownLegacy(content, options);
}
}
/**
* Legacy processing function as fallback
*
* This function contains the original processing logic and serves as a
* reliable fallback if the new pipeline system encounters issues.
*/
async function processLegalMarkdownLegacy(content, options = {}) {
// Clear field tracker for new document
fieldTracker.clear();
// Convert RST or LaTeX to legal markdown if needed (before YAML parsing)
let preprocessedContent = await convertRstToLegalMarkdown(content);
preprocessedContent = await convertLatexToLegalMarkdown(preprocessedContent);
// Parse YAML Front Matter
const { content: contentWithoutYaml, metadata } = parseYamlFrontMatter(preprocessedContent, options.throwOnYamlError);
// If only processing YAML, return early
if (options.yamlOnly) {
return { content: contentWithoutYaml, metadata };
}
// Process partial imports (must be done early)
let processedContent = contentWithoutYaml;
let exportedFiles = [];
if (!options.noImports) {
const importResult = processPartialImports(processedContent, options.basePath, metadata, options);
processedContent = importResult.content;
exportedFiles = importResult.importedFiles;
// Merge imported frontmatter unless explicitly disabled
if (options.disableFrontmatterMerge !== true && importResult.mergedMetadata) {
Object.assign(metadata, importResult.mergedMetadata);
}
}
// Process optional clauses
if (!options.noClauses) {
processedContent = processOptionalClauses(processedContent, metadata);
}
// Process cross references
if (!options.noReferences) {
processedContent = processCrossReferences(processedContent, metadata);
}
// Process mixins first (AST-based, no contamination)
if (!options.noMixins) {
processedContent = processMixins(processedContent, metadata, options);
}
// Process template loops after mixins (now separated from mixin processor)
processedContent = processTemplateLoops(processedContent, metadata, undefined, options.enableFieldTrackingInMarkdown || false);
// Process headers (numbering, etc)
if (!options.noHeaders) {
processedContent = processHeaders(processedContent, metadata, {
noReset: options.noReset,
noIndent: options.noIndent,
enableFieldTrackingInMarkdown: options.enableFieldTrackingInMarkdown,
});
}
// Export metadata if requested or specified in metadata
if (options.exportMetadata ||
metadata['meta-yaml-output'] ||
metadata['meta-json-output']) {
const exportResult = exportMetadata(metadata, options.exportFormat, options.exportPath);
exportedFiles = [...exportedFiles, ...exportResult.exportedFiles];
}
// Apply field tracking to content if highlighting is enabled
// NOTE: This applies post-processing field tracking for HTML/PDF output
// Individual processors use enableFieldTrackingInMarkdown for markdown output
// Skip if enableFieldTrackingInMarkdown is true (already processed by AST processor)
if (options.enableFieldTracking && !options.enableFieldTrackingInMarkdown) {
processedContent = fieldTracker.applyFieldTracking(processedContent);
}
return {
content: processedContent,
metadata,
exportedFiles,
fieldReport: options.enableFieldTracking ? fieldTracker.generateReport() : undefined,
};
}
/**
* Main function to process a Legal Markdown document (sync version with fallback)
*
* This function uses the legacy processing approach to maintain synchronous operation.
* For better performance, debugging, and features, consider using the async version
* `processLegalMarkdownAsync` which uses the new pipeline system.
*
* @param {string} content - The raw Legal Markdown content to process
* @param {LegalMarkdownOptions} [options={}] - Configuration options for processing
* @returns {Object} Processing result containing processed content, metadata, and reports
* @returns {string} returns.content - The processed document content
* @returns {Record<string, any>} [returns.metadata] - Extracted YAML metadata
* @returns {string[]} [returns.exportedFiles] - Array of exported metadata files
* @returns {Object} [returns.fieldReport] - Field tracking report if enabled
* @example
* ```typescript
* const result = processLegalMarkdown(content, {
* enableFieldTracking: true,
* basePath: './documents',
* noClauses: false,
* noReferences: false
* });
*
* console.log(result.content); // Processed markdown
* console.log(result.metadata); // YAML front matter
* console.log(result.fieldReport); // Field usage report
* ```
*/
export function processLegalMarkdown(content, options = {}) {
// Clear field tracker for new document
fieldTracker.clear();
// Convert RST or LaTeX to legal markdown if needed (before YAML parsing)
let preprocessedContent = convertRstToLegalMarkdownSync(content);
preprocessedContent = convertLatexToLegalMarkdownSync(preprocessedContent);
// Parse YAML Front Matter
const { content: contentWithoutYaml, metadata } = parseYamlFrontMatter(preprocessedContent, options.throwOnYamlError);
// If only processing YAML, return early
if (options.yamlOnly) {
return { content: contentWithoutYaml, metadata };
}
// Process partial imports (must be done early)
let processedContent = contentWithoutYaml;
let exportedFiles = [];
if (!options.noImports) {
const importResult = processPartialImports(processedContent, options.basePath, metadata, options);
processedContent = importResult.content;
exportedFiles = importResult.importedFiles;
// Merge imported frontmatter unless explicitly disabled
if (options.disableFrontmatterMerge !== true && importResult.mergedMetadata) {
Object.assign(metadata, importResult.mergedMetadata);
}
}
// Process optional clauses
if (!options.noClauses) {
processedContent = processOptionalClauses(processedContent, metadata);
}
// Process cross references
if (!options.noReferences) {
processedContent = processCrossReferences(processedContent, metadata);
}
// Process mixins first (AST-based, no contamination)
if (!options.noMixins) {
processedContent = processMixins(processedContent, metadata, options);
}
// Process template loops after mixins (now separated from mixin processor)
processedContent = processTemplateLoops(processedContent, metadata, undefined, options.enableFieldTrackingInMarkdown || false);
// Process headers (numbering, etc)
if (!options.noHeaders) {
processedContent = processHeaders(processedContent, metadata, {
noReset: options.noReset,
noIndent: options.noIndent,
enableFieldTrackingInMarkdown: options.enableFieldTrackingInMarkdown,
});
}
// Export metadata if requested or specified in metadata
if (options.exportMetadata || metadata['meta-yaml-output'] || metadata['meta-json-output']) {
// Use exportPath if specified, otherwise fall back to basePath for better relative path resolution
const effectiveExportPath = options.exportPath || options.basePath;
const exportResult = exportMetadata(metadata, options.exportFormat, effectiveExportPath);
exportedFiles = [...exportedFiles, ...exportResult.exportedFiles];
}
// Apply field tracking to content if highlighting is enabled
// NOTE: This applies post-processing field tracking for HTML/PDF output
// Individual processors use enableFieldTrackingInMarkdown for markdown output
// Skip if enableFieldTrackingInMarkdown is true (already processed by AST processor)
if (options.enableFieldTracking && !options.enableFieldTrackingInMarkdown) {
processedContent = fieldTracker.applyFieldTracking(processedContent);
}
return {
content: processedContent,
metadata,
exportedFiles,
fieldReport: options.enableFieldTracking ? fieldTracker.generateReport() : undefined,
};
}
/**
* Generate HTML from Legal Markdown content
*
* This function processes Legal Markdown content and generates a complete HTML
* document with styling, field highlighting, and responsive design features.
* It combines the Legal Markdown processing pipeline with HTML generation.
*
* @param {string} content - The raw Legal Markdown content to convert
* @param {LegalMarkdownOptions & Object} [options={}] - Configuration options
* @param {string} [options.cssPath] - Path to custom CSS file
* @param {string} [options.highlightCssPath] - Path to field highlighting CSS
* @param {boolean} [options.includeHighlighting] - Whether to include field highlighting
* @param {string} [options.title] - Document title for HTML
* @returns {Promise<string>} A promise that resolves to the complete HTML document
* @throws {Error} When HTML generation fails
* @example
* ```typescript
* const html = await generateHtml(content, {
* title: 'Service Agreement',
* cssPath: './custom-styles.css',
* includeHighlighting: true,
* enableFieldTracking: true
* });
* ```
*/
export async function generateHtml(content, options = {}) {
try {
// Use HTML-optimized pipeline for better performance and field tracking
// Field tracking is always enabled for HTML generation
const pipeline = createHtmlPipeline({
enableFieldTracking: true,
includeHighlighting: options.includeHighlighting,
});
const metadata = {};
const pipelineOptions = {
legalMarkdownOptions: {
...options,
enableFieldTracking: true,
enableFieldTrackingInMarkdown: true, // Always enabled for HTML generation (structure)
_htmlGeneration: true, // Flag to indicate HTML generation context
},
enableStepProfiling: process.env.NODE_ENV === 'development',
};
const result = await pipeline.execute(content, metadata, pipelineOptions);
if (!result.success) {
console.warn('HTML pipeline failed, falling back to legacy processing');
return generateHtmlLegacy(content, options);
}
// Generate HTML using the processed content
return htmlGenerator.generateHtml(result.content, {
cssPath: options.cssPath,
highlightCssPath: options.highlightCssPath || path.join(process.cwd(), 'src/styles/highlight.css'),
includeHighlighting: options.includeHighlighting,
title: options.title,
metadata: result.metadata,
});
}
catch (error) {
console.warn('HTML generation pipeline failed, falling back to legacy processing:', error);
return generateHtmlLegacy(content, options);
}
}
/**
* Legacy HTML generation as fallback
*/
async function generateHtmlLegacy(content, options = {}) {
// Process the legal markdown first (use async version for better RST/LaTeX support)
const processed = await processLegalMarkdownAsync(content, {
...options,
enableFieldTracking: true,
enableFieldTrackingInMarkdown: true, // Always enabled for HTML legacy generation (structure)
});
// Generate HTML
return htmlGenerator.generateHtml(processed.content, {
cssPath: options.cssPath,
highlightCssPath: options.highlightCssPath || path.join(process.cwd(), 'src/styles/highlight.css'),
includeHighlighting: options.includeHighlighting,
title: options.title,
metadata: processed.metadata,
});
}
/**
* Generate PDF from Legal Markdown content
*
* This function processes Legal Markdown content and generates a PDF document
* with professional styling, field highlighting, and customizable page formatting.
* It combines the Legal Markdown processing pipeline with PDF generation.
*
* @param {string} content - The raw Legal Markdown content to convert
* @param {string} outputPath - File path where the PDF will be saved
* @param {LegalMarkdownOptions & Object} [options={}] - Configuration options
* @param {string} [options.cssPath] - Path to custom CSS file
* @param {string} [options.highlightCssPath] - Path to field highlighting CSS
* @param {boolean} [options.includeHighlighting] - Whether to include field highlighting
* @param {string} [options.title] - Document title for PDF
* @param {'A4' | 'Letter' | 'Legal'} [options.format] - Page format
* @param {boolean} [options.landscape] - Whether to use landscape orientation
* @returns {Promise<Buffer>} A promise that resolves to the PDF buffer
* @throws {Error} When PDF generation fails
* @example
* ```typescript
* const pdf = await generatePdf(content, './contract.pdf', {
* title: 'Service Agreement',
* format: 'A4',
* includeHighlighting: true,
* enableFieldTracking: true
* });
* ```
*/
export async function generatePdf(content, outputPath, options = {}) {
try {
// Use PDF-optimized pipeline (same as HTML pipeline for now)
// Field tracking is always enabled for PDF generation
const pipeline = createHtmlPipeline({
enableFieldTracking: true,
includeHighlighting: options.includeHighlighting,
});
const metadata = {};
const pipelineOptions = {
legalMarkdownOptions: {
...options,
enableFieldTracking: true,
enableFieldTrackingInMarkdown: true, // Always enabled for PDF generation (structure)
_htmlGeneration: true, // Flag to indicate PDF generation context
},
enableStepProfiling: process.env.NODE_ENV === 'development',
};
const result = await pipeline.execute(content, metadata, pipelineOptions);
if (!result.success) {
console.warn('PDF pipeline failed, falling back to legacy processing');
return generatePdfLegacy(content, outputPath, options);
}
// Generate PDF using the processed content
return pdfGenerator.generatePdf(result.content, outputPath, {
cssPath: options.cssPath,
highlightCssPath: options.highlightCssPath || path.join(process.cwd(), 'src/styles/highlight.css'),
includeHighlighting: options.includeHighlighting,
title: options.title,
metadata: result.metadata,
format: options.format,
landscape: options.landscape,
});
}
catch (error) {
console.warn('PDF generation pipeline failed, falling back to legacy processing:', error);
return generatePdfLegacy(content, outputPath, options);
}
}
/**
* Legacy PDF generation as fallback
*/
async function generatePdfLegacy(content, outputPath, options = {}) {
// Process the legal markdown first (use async version for better RST/LaTeX support)
const processed = await processLegalMarkdownAsync(content, {
...options,
enableFieldTracking: true,
enableFieldTrackingInMarkdown: true, // Always enabled for PDF legacy generation (structure)
});
// Generate PDF
return pdfGenerator.generatePdf(processed.content, outputPath, {
cssPath: options.cssPath,
highlightCssPath: options.highlightCssPath || path.join(process.cwd(), 'src/styles/highlight.css'),
includeHighlighting: options.includeHighlighting,
title: options.title,
metadata: processed.metadata,
format: options.format,
landscape: options.landscape,
});
}
/**
* Generate both normal and highlighted PDF versions
*
* This function creates two PDF versions of the same Legal Markdown document:
* one with standard formatting and another with field highlighting enabled.
* This is useful for document review processes where both clean and annotated
* versions are needed.
*
* @param {string} content - The raw Legal Markdown content to convert
* @param {string} outputPath - Base file path for PDFs (will be modified for each version)
* @param {LegalMarkdownOptions & Object} [options={}] - Configuration options
* @param {string} [options.cssPath] - Path to custom CSS file
* @param {string} [options.highlightCssPath] - Path to field highlighting CSS
* @param {string} [options.title] - Document title for PDFs
* @param {'A4' | 'Letter' | 'Legal'} [options.format] - Page format
* @param {boolean} [options.landscape] - Whether to use landscape orientation
* @returns {Promise<Object>} A promise that resolves to both PDF buffers
* @returns {Buffer} returns.normal - The normal PDF without highlighting
* @returns {Buffer} returns.highlighted - The highlighted PDF with field annotations
* @throws {Error} When PDF generation fails
* @example
* ```typescript
* const { normal, highlighted } = await generatePdfVersions(content, './contract.pdf', {
* title: 'Service Agreement',
* format: 'A4'
* });
* // Creates: contract.pdf and contract.HIGHLIGHT.pdf
* ```
*/
export async function generatePdfVersions(content, outputPath, options = {}) {
const normalPath = outputPath;
const highlightedPath = outputPath.replace('.pdf', '.HIGHLIGHT.pdf');
const [normal, highlighted] = await Promise.all([
generatePdf(content, normalPath, { ...options, includeHighlighting: false }),
generatePdf(content, highlightedPath, { ...options, includeHighlighting: true }),
]);
return { normal, highlighted };
}
// Export all sub-modules
export * from './types.js';
export * from './core/index.js';
export * from './errors/index.js';
export * from './constants/index.js';
export * from './utils/index.js';
export * from './extensions/index.js';
// Specific re-exports to avoid conflicts (extensions take precedence)
export { fieldTracker } from './extensions/tracking/field-tracker.js';
export { htmlGenerator } from './extensions/generators/html-generator.js';
export { pdfGenerator } from './extensions/generators/pdf-generator.js';
// Remark-based processing functions
export { processLegalMarkdownWithRemark, processLegalMarkdownWithRemarkSync, } from './extensions/remark/legal-markdown-processor.js';
/**
* Wrapper function that provides legacy API compatibility with remark processing
*
* This function bridges the gap between the legacy `processLegalMarkdown` interface
* and the new remark-based processor. It maintains 100% API compatibility while
* internally using the modern remark pipeline.
*
* @param content - The raw Legal Markdown content to process
* @param options - Legacy LegalMarkdownOptions (will be mapped to remark options)
* @returns Processing result in legacy format
*/
//# sourceMappingURL=index.js.map