UNPKG

legal-markdown-js

Version:

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

215 lines 8.56 kB
/** * Optional Clause Processing Module for Legal Markdown Documents * * This module provides functionality to process conditional clauses in Legal Markdown * documents, allowing for dynamic content inclusion based on metadata conditions. * It supports complex boolean logic with AND/OR operations, equality comparisons, * and nested value access for sophisticated document customization. * * Features: * - Conditional clause syntax: [content]{condition} * - Boolean logic with AND/OR operations * - Equality and inequality comparisons * - Nested metadata value access with dot notation * - Type-aware value comparison (strings, numbers, booleans) * - Graceful error handling for invalid conditions * - Support for quoted string values in conditions * * @example * ```typescript * import { processOptionalClauses } from './clause-processor.js'; * * const content = ` * This agreement [includes confidentiality clauses]{confidentiality} * [and has a termination period of {{termination_days}} days]{termination_days} * [with special provisions for European clients]{client.region = "EU"}. * `; * * const metadata = { * confidentiality: true, * termination_days: 30, * client: { region: "EU" } * }; * * const processed = processOptionalClauses(content, metadata); * console.log(processed); * // Output: * // This agreement includes confidentiality clauses * // and has a termination period of 30 days * // with special provisions for European clients. * ``` * * @module */ /** * Processes optional clauses in a LegalMarkdown document * * Evaluates conditional clauses using the syntax [content]{condition} and includes * or excludes content based on metadata values. Supports complex boolean logic * and various comparison operators for sophisticated document customization. * * @param {string} content - The document content containing optional clauses * @param {Record<string, any>} metadata - Document metadata with clause conditions * @returns {string} Processed content with conditional clauses evaluated * @example * ```typescript * // Simple boolean condition * const content1 = '[This clause is included]{include_special}'; * const metadata1 = { include_special: true }; * const result1 = processOptionalClauses(content1, metadata1); * // Output: "This clause is included" * * // Complex condition with AND/OR logic * const content2 = '[Premium features available]{premium = true AND region != "restricted"}'; * const metadata2 = { premium: true, region: "US" }; * const result2 = processOptionalClauses(content2, metadata2); * // Output: "Premium features available" * * // Nested metadata access * const content3 = '[European compliance required]{client.location.country = "DE"}'; * const metadata3 = { client: { location: { country: "DE" } } }; * const result3 = processOptionalClauses(content3, metadata3); * // Output: "European compliance required" * ``` */ export function processOptionalClauses(content, metadata) { // Regular expression to match optional clauses // Format: [Optional text]{condition} // Note: We remove the 's' flag to prevent matching across large sections of text // and add \n to prevent matching across too many lines // eslint-disable-next-line no-useless-escape const optionalClausePattern = /\[([^[\]]*(?:\[[^\]]*\][^[\]]*)*?)\]\{([^{}]*?)\}/g; return content.replace(optionalClausePattern, (match, text, condition) => { // Evaluate the condition const shouldInclude = evaluateCondition(condition, metadata); // Include the text if condition is true, otherwise return empty string return shouldInclude ? text : ''; }); } /** * Evaluates a condition expression against metadata * * Parses and evaluates condition expressions that can include simple variable * references, equality/inequality comparisons, and boolean logic with AND/OR * operations. Supports nested metadata access using dot notation. * * @private * @param {string} condition - Condition expression to evaluate * @param {Record<string, any>} metadata - Document metadata to test against * @returns {boolean} True if condition evaluates to true, false otherwise * @example * ```typescript * const metadata = { * active: true, * user: { role: "admin", level: 5 }, * region: "US" * }; * * console.log(evaluateCondition("active", metadata)); // true * console.log(evaluateCondition("user.role = \"admin\"", metadata)); // true * console.log(evaluateCondition("user.level != 3", metadata)); // true * console.log(evaluateCondition("active AND region = \"US\"", metadata)); // true * console.log(evaluateCondition("active OR region = \"EU\"", metadata)); // true * ``` */ function evaluateCondition(condition, metadata) { // Handle empty condition (always true) if (!condition.trim()) { return true; } // Simple variable reference (e.g., "include_clause") if (!condition.includes('=') && !condition.includes('!') && !condition.includes('AND') && !condition.includes('OR')) { const value = getNestedValue(metadata, condition.trim()); return Boolean(value); } // Complex conditions try { // Handle logical AND first (higher precedence in processing) if (condition.includes(' AND ')) { const subConditions = condition.split(' AND ').map(c => c.trim()); return subConditions.every(cond => evaluateCondition(cond, metadata)); } // Handle logical OR if (condition.includes(' OR ')) { const subConditions = condition.split(' OR ').map(c => c.trim()); return subConditions.some(cond => evaluateCondition(cond, metadata)); } // Handle equality/inequality (after logical operators) if (condition.includes('=')) { // Parse "key = value" or "key != value" const isNotEqual = condition.includes('!='); const parts = condition.split(isNotEqual ? '!=' : '=').map(p => p.trim()); if (parts.length !== 2) { return false; } const [key, valueStr] = parts; const metadataValue = getNestedValue(metadata, key); // Parse the value string (handle quoted strings, booleans, numbers) let expectedValue = valueStr; if (valueStr.startsWith('"') && valueStr.endsWith('"')) { expectedValue = valueStr.slice(1, -1); } else if (valueStr === 'true') { expectedValue = true; } else if (valueStr === 'false') { expectedValue = false; } else if (!isNaN(Number(valueStr))) { expectedValue = Number(valueStr); } return isNotEqual ? metadataValue !== expectedValue : metadataValue === expectedValue; } // Default to false for unknown condition formats return false; } catch (error) { console.error('Error evaluating condition:', error); return false; } } /** * Gets a potentially nested value from an object * * Traverses an object using dot notation to access nested properties safely. * Returns undefined if any part of the path doesn't exist, preventing errors * when accessing deeply nested properties. * * @private * @param {Record<string, any>} obj - Object to extract value from * @param {string} path - Dot-separated path to the value (e.g., "user.profile.name") * @returns {any} The value at the specified path, or undefined if not found * @example * ```typescript * const obj = { * user: { * profile: { * name: "John", * settings: { theme: "dark" } * }, * active: true * } * }; * * console.log(getNestedValue(obj, "user.profile.name")); // "John" * console.log(getNestedValue(obj, "user.active")); // true * console.log(getNestedValue(obj, "user.profile.settings.theme")); // "dark" * console.log(getNestedValue(obj, "user.nonexistent")); // undefined * console.log(getNestedValue(obj, "missing.path")); // undefined * ``` */ function getNestedValue(obj, path) { const keys = path.split('.'); let value = obj; for (const key of keys) { if (value === undefined || value === null) { return undefined; } value = value[key]; } return value; } //# sourceMappingURL=clause-processor.js.map