UNPKG

legal-markdown-js

Version:

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

346 lines 13.5 kB
/** * Date Processing Module for Legal Markdown Documents * * This module provides comprehensive date processing functionality for Legal Markdown * documents, supporting the `@today` syntax with various formatting options. It handles * different date formats, timezones, locales, and provides specialized formatting * for legal documents including ordinal suffixes and custom format patterns. * * Features: * - `@today` syntax for current date insertion * - Format override support: `@today[format]` * - Multiple predefined formats (ISO, US, European, legal, etc.) * - Custom format pattern parsing (YYYY-MM-DD, etc.) * - Timezone and locale support * - Ordinal suffix generation for legal formats * - Graceful error handling with fallback formatting * - Integration with metadata for default settings * * @example * ```typescript * import { processDateReferences } from './date-processor.js'; * * const content = ` * This document is dated @today. * Contract effective date: @today[long] * Expiration: @today[YYYY-MM-DD] * Legal format: @today[legal] * `; * * const metadata = { * 'date-format': 'ISO', * 'timezone': 'America/New_York', * 'locale': 'en-US' * }; * * const processed = processDateReferences(content, metadata); * console.log(processed); * // Output: * // This document is dated 2024-01-15. * // Contract effective date: January 15, 2024 * // Expiration: 2024-01-15 * // Legal format: January 15th, 2024 * ``` */ import { ParseError } from '../../errors/index.js'; import { logger } from '../../utils/logger.js'; /** * Processes special date references in legal documents * * This is the main function that processes `@today` references in Legal Markdown * documents. It supports format overrides and uses metadata settings for * default formatting, timezone, and locale preferences. * * @deprecated This function is deprecated and will be removed in v4.0.0. * Use `processLegalMarkdownWithRemark()` with the `remarkDates` plugin instead. * The remark-based approach provides better AST processing and date handling. * @see {@link https://github.com/yourrepo/legal-markdown-js/blob/main/docs/migration-guide.md Migration Guide} * * @param {string} content - The document content containing `@today` references * @param {Record<string, any>} metadata - Document metadata with date formatting options * @returns {string} Processed content with `@today` references replaced by formatted dates * @example * ```typescript * // Basic usage with default format * const content1 = "Document dated `@today`"; * const result1 = processDateReferences(content1, {}); * // Output: "Document dated 2024-01-15" * * // Format override * const content2 = "Contract effective `@today[long]`"; * const result2 = processDateReferences(content2, {}); * // Output: "Contract effective January 15, 2024" * * // Using metadata settings * const content3 = "Generated `@today`"; * const metadata = { 'date-format': 'legal', 'timezone': 'America/New_York' }; * const result3 = processDateReferences(content3, metadata); * // Output: "Generated January 15th, 2024" * ``` */ export function processDateReferences(content, metadata) { // DEPRECATION WARNING if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'test') { logger.warn('processDateReferences() is deprecated and will be removed in v4.0.0. ' + 'Use processLegalMarkdownWithRemark() with remarkDates plugin instead. ' + 'See: https://github.com/yourrepo/legal-markdown-js/blob/main/docs/migration-guide.md'); } // Extract date format options from metadata const dateOptions = extractDateOptions(metadata); // Regular expression to match `@today` references const todayPattern = /@today(?:\[([^\]]+)\])?/g; return content.replace(todayPattern, (match, formatOverride) => { // Use format override if provided, otherwise use metadata format, otherwise use default const format = formatOverride || dateOptions.dateFormat || 'YYYY-MM-DD'; const timezone = dateOptions.timezone || 'UTC'; const locale = dateOptions.locale || 'en-US'; return formatDate(new Date(), format, timezone, locale); }); } /** * Extracts date formatting options from metadata * * Parses document metadata to extract date formatting configuration, * supporting both hyphenated and camelCase property names for flexibility. * * @private * @param {Record<string, any>} metadata - Document metadata containing date options * @returns {DateFormatOptions} Extracted date formatting options * @example * ```typescript * const metadata = { * 'date-format': 'legal', * 'timezone': 'America/New_York', * 'locale': 'en-US' * }; * * const options = extractDateOptions(metadata); * // Returns: { dateFormat: 'legal', timezone: 'America/New_York', locale: 'en-US' } * ``` */ function extractDateOptions(metadata) { const asString = (v) => (typeof v === 'string' ? v : undefined); return { dateFormat: asString(metadata['date-format']) || asString(metadata.dateFormat), timezone: asString(metadata['timezone']) || asString(metadata.tz), locale: asString(metadata['locale']) || asString(metadata.lang), }; } /** * Formats a date according to the specified format, timezone, and locale * * Converts a Date object to a formatted string using various predefined formats * or custom format patterns. Supports timezone conversion and locale-specific * formatting with fallback to ISO format on errors. * * @private * @param {Date} date - The date to format * @param {string} format - Format specification (ISO, US, European, legal, long, etc.) * @param {string} timezone - Timezone identifier (e.g., 'America/New_York') * @param {string} locale - Locale identifier (e.g., 'en-US') * @returns {string} Formatted date string * @example * ```typescript * const date = new Date('2024-01-15'); * * console.log(formatDate(date, 'ISO', 'UTC', 'en-US')); // '2024-01-15' * console.log(formatDate(date, 'US', 'UTC', 'en-US')); // '01/15/2024' * console.log(formatDate(date, 'legal', 'UTC', 'en-US')); // 'January 15th, 2024' * console.log(formatDate(date, 'YYYY-MM-DD', 'UTC', 'en-US')); // '2024-01-15' * ``` */ function formatDate(date, format, timezone, locale) { try { // Create a new date in the specified timezone const options = { timeZone: timezone, }; // Handle different format patterns switch (format.toLowerCase()) { case 'iso': case 'yyyy-mm-dd': options.year = 'numeric'; options.month = '2-digit'; options.day = '2-digit'; return new Intl.DateTimeFormat('en-CA', options).format(date); case 'us': case 'mm/dd/yyyy': options.year = 'numeric'; options.month = '2-digit'; options.day = '2-digit'; return new Intl.DateTimeFormat('en-US', options).format(date); case 'european': case 'dd/mm/yyyy': options.year = 'numeric'; options.month = '2-digit'; options.day = '2-digit'; return new Intl.DateTimeFormat('en-GB', options).format(date); case 'long': options.year = 'numeric'; options.month = 'long'; options.day = 'numeric'; return new Intl.DateTimeFormat(locale, options).format(date); case 'medium': options.year = 'numeric'; options.month = 'short'; options.day = 'numeric'; return new Intl.DateTimeFormat(locale, options).format(date); case 'short': options.year = '2-digit'; options.month = 'short'; options.day = 'numeric'; return new Intl.DateTimeFormat(locale, options).format(date); case 'legal': { // Legal format: "January 1st, 2024" options.year = 'numeric'; options.month = 'long'; options.day = 'numeric'; const formatted = new Intl.DateTimeFormat(locale, options).format(date); return addOrdinalSuffix(formatted, date.getDate()); } default: // Try to parse custom format patterns try { return parseCustomFormat(date, format, timezone, locale); } catch { // If custom format parsing fails, fallback to ISO return date.toISOString().split('T')[0]; } } } catch { // Fallback to ISO format if there's an error return date.toISOString().split('T')[0]; } } /** * Adds ordinal suffix to legal format (1st, 2nd, 3rd, etc.) * * Modifies a formatted date string to include ordinal suffixes for the day, * used specifically in legal document formatting. * * @private * @param {string} formatted - The formatted date string * @param {number} day - The day number to add ordinal suffix to * @returns {string} Date string with ordinal suffix * @example * ```typescript * const formatted = addOrdinalSuffix('January 1, 2024', 1); * // Returns: 'January 1st, 2024' * ``` */ function addOrdinalSuffix(formatted, day) { const suffix = getOrdinalSuffix(day); return formatted.replace(/\b\d+\b/, `${day}${suffix}`); } /** * Gets ordinal suffix for a number * * Determines the appropriate ordinal suffix (st, nd, rd, th) for a given number * following English ordinal number rules. * * @private * @param {number} num - The number to get ordinal suffix for * @returns {string} The ordinal suffix (st, nd, rd, or th) * @example * ```typescript * console.log(getOrdinalSuffix(1)); // 'st' * console.log(getOrdinalSuffix(2)); // 'nd' * console.log(getOrdinalSuffix(3)); // 'rd' * console.log(getOrdinalSuffix(4)); // 'th' * console.log(getOrdinalSuffix(11)); // 'th' * console.log(getOrdinalSuffix(21)); // 'st' * ``` */ function getOrdinalSuffix(num) { const lastDigit = num % 10; const lastTwoDigits = num % 100; if (lastTwoDigits >= 11 && lastTwoDigits <= 13) { return 'th'; } switch (lastDigit) { case 1: return 'st'; case 2: return 'nd'; case 3: return 'rd'; default: return 'th'; } } /** * Parses custom date format patterns * * Processes custom date format strings using token replacement patterns. * Supports various tokens like YYYY, MM, DD, etc., and throws an error * for invalid or unrecognized format patterns. * * @private * @param {Date} date - The date to format * @param {string} format - Custom format pattern string * @param {string} timezone - Timezone identifier * @param {string} locale - Locale identifier * @returns {string} Formatted date string * @throws {Error} When format pattern is invalid or contains unrecognized tokens * @example * ```typescript * const date = new Date('2024-01-15'); * * console.log(parseCustomFormat(date, 'YYYY-MM-DD', 'UTC', 'en-US')); // '2024-01-15' * console.log(parseCustomFormat(date, 'DD/MM/YYYY', 'UTC', 'en-US')); // '15/01/2024' * console.log(parseCustomFormat(date, 'MMMM D, YYYY', 'UTC', 'en-US')); // 'January 15, 2024' * ``` */ function parseCustomFormat(date, format, timezone, locale) { const options = { timeZone: timezone, }; // Define token patterns and their formatters const tokenPatterns = [ { pattern: /YYYY/g, formatter: () => new Intl.DateTimeFormat(locale, { ...options, year: 'numeric' }).format(date), }, { pattern: /YY/g, formatter: () => new Intl.DateTimeFormat(locale, { ...options, year: '2-digit' }).format(date), }, { pattern: /MMMM/g, formatter: () => new Intl.DateTimeFormat(locale, { ...options, month: 'long' }).format(date), }, { pattern: /MMM/g, formatter: () => new Intl.DateTimeFormat(locale, { ...options, month: 'short' }).format(date), }, { pattern: /MM/g, formatter: () => new Intl.DateTimeFormat(locale, { ...options, month: '2-digit' }).format(date), }, { pattern: /\bM\b/g, formatter: () => new Intl.DateTimeFormat(locale, { ...options, month: 'numeric' }).format(date), }, { pattern: /DD/g, formatter: () => new Intl.DateTimeFormat(locale, { ...options, day: '2-digit' }).format(date), }, { pattern: /\bD\b/g, formatter: () => new Intl.DateTimeFormat(locale, { ...options, day: 'numeric' }).format(date), }, ]; // Apply token replacements let result = format; for (const { pattern, formatter } of tokenPatterns) { result = result.replace(pattern, formatter()); } // If result still contains unreplaced tokens of our pattern, it's an invalid format if (result.match(/\b[YMDH]+\b/) || result === format) { throw new ParseError('Invalid format pattern', { format }); } return result; } //# sourceMappingURL=date-processor.js.map