legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
339 lines • 10.6 kB
JavaScript
/**
* @fileoverview Object Flattening Utilities for Frontmatter Merge
*
* This module provides utilities to flatten hierarchical objects into dot notation
* and reconstruct them back to nested objects. Used for granular frontmatter merging
* where individual properties can be compared and merged independently.
*
* Features:
* - Flatten nested objects to dot notation (e.g., {a: {b: 1}} → {"a.b": 1})
* - Unflatten dot notation back to nested objects
* - Handle arrays as atomic values
* - Preserve null/undefined values
* - Type-safe operations
*
* @example
* ```typescript
* import { flattenObject, unflattenObject } from './object-flattener.js';
*
* const nested = {
* config: {
* level: "high",
* options: {
* debug: true
* }
* },
* items: [1, 2, 3]
* };
*
* const flattened = flattenObject(nested);
* // {
* // "config.level": "high",
* // "config.options.debug": true,
* // "items": [1, 2, 3]
* // }
*
* const restored = unflattenObject(flattened);
* // Back to original nested structure
* ```
*/
import { ProcessingError } from '../../errors/index.js';
import { logger } from '../../utils/logger.js';
/**
* Numeric typed array constructors (Issue #141)
* These types should be treated as atomic values during flattening/merging
*/
export const NUMERIC_TYPED_ARRAY_TYPES = [
Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
Float32Array,
Float64Array,
];
/**
* Checks if a value is a Buffer instance
* Handles Node.js Buffer availability check
*
* @param value - Value to check
* @returns True if value is a Buffer instance
*/
export function isBuffer(value) {
return typeof Buffer !== 'undefined' && value instanceof Buffer;
}
/**
* Checks if a value is a BigInt typed array
* Handles BigInt typed array availability and TypeScript type compatibility
*
* @param value - Value to check
* @returns True if value is a BigInt typed array
*/
export function isBigIntTypedArray(value) {
if (typeof BigInt64Array !== 'undefined' && value instanceof BigInt64Array) {
return true;
}
if (typeof BigUint64Array !== 'undefined' && value instanceof BigUint64Array) {
return true;
}
return false;
}
/**
* Checks if a value is any typed array (numeric or BigInt)
*
* @param value - Value to check
* @returns True if value is a typed array
*/
function isTypedArray(value) {
return NUMERIC_TYPED_ARRAY_TYPES.some(Type => value instanceof Type) || isBigIntTypedArray(value);
}
/**
* Checks if a value should be treated as atomic (not flattened)
*
* Atomic values include primitives, arrays, and special object types like Date, RegExp,
* Error, Buffer, and typed arrays. These should not be recursively flattened because
* they have special semantics that would be lost during flattening.
*
* @param value - Value to check
* @returns True if value should be treated as atomic, false if it should be flattened
*
* @example
* ```typescript
* isAtomicValue(null); // true (primitive)
* isAtomicValue([1, 2, 3]); // true (array)
* isAtomicValue(new Date()); // true (special object type)
* isAtomicValue({ a: 1 }); // false (plain object - should flatten)
* ```
*/
function isAtomicValue(value) {
// Primitives and null/undefined
if (value === null || value === undefined)
return true;
if (typeof value !== 'object')
return true; // string, number, boolean, etc.
// Arrays are atomic
if (Array.isArray(value))
return true;
// Special object types that should be treated as atomic (Issue #141)
if (value instanceof Date)
return true;
if (value instanceof RegExp)
return true;
if (value instanceof Error)
return true;
// Node.js Buffer
if (isBuffer(value))
return true;
// Collections
if (value instanceof Set || value instanceof WeakSet)
return true;
if (value instanceof Map || value instanceof WeakMap)
return true;
// Typed arrays (numeric and BigInt)
if (isTypedArray(value))
return true;
// Regular plain object - should be flattened
return false;
}
/**
* Converts a hierarchical object to dot notation
*
* Recursively traverses object properties and creates flat keys using dot notation.
* Arrays and special object types (Date, RegExp, Error, Buffer, etc.) are treated as
* atomic values and not flattened.
*
* @param obj - Object to flatten
* @param prefix - Current prefix for nested properties (used internally)
* @param visited - WeakSet for circular reference detection (used internally)
* @param startTime - Start timestamp for timeout detection (used internally)
* @param timeoutMs - Maximum execution time in milliseconds (default: 5000ms)
* @returns Flattened object with dot notation keys
* @throws Error if operation times out or circular reference detected
*
* @example
* ```typescript
* const nested = {
* client: {
* name: "Acme Corp",
* contact: {
* email: "contact@acme.com"
* }
* },
* tags: ["legal", "contract"],
* created: new Date("2025-01-15")
* };
*
* const result = flattenObject(nested);
* // {
* // "client.name": "Acme Corp",
* // "client.contact.email": "contact@acme.com",
* // "tags": ["legal", "contract"],
* // "created": Date object (preserved, not flattened)
* // }
* ```
*/
export function flattenObject(obj, prefix = '', visited = new WeakSet(), startTime = Date.now(), timeoutMs = 5000) {
// Timeout safety check
if (Date.now() - startTime > timeoutMs) {
const message = `Object flattening timed out after ${timeoutMs}ms. ` +
'This may indicate a complex nested structure or circular references.';
throw new ProcessingError(message);
}
if (obj === null || obj === undefined) {
return {};
}
const flattened = {};
// Handle atomic values (primitives, arrays, special types)
if (isAtomicValue(obj)) {
if (prefix) {
flattened[prefix] = obj;
}
return flattened;
}
// At this point obj is a plain object (not atomic, not null/undefined)
const objRecord = obj;
// Circular reference detection
if (visited.has(objRecord)) {
logger.warn(`Circular reference detected at path '${prefix}'. Replacing with placeholder.`);
if (prefix) {
flattened[prefix] = '[Circular Reference]';
}
return flattened;
}
visited.add(objRecord);
// Process object properties
for (const [key, value] of Object.entries(objRecord)) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (isAtomicValue(value)) {
// Treat as atomic value (includes null, undefined, primitives, arrays, Date, RegExp, etc.)
flattened[newKey] = value;
}
else {
// Recursively flatten nested plain objects only
Object.assign(flattened, flattenObject(value, newKey, visited, startTime, timeoutMs));
}
}
visited.delete(objRecord);
return flattened;
}
/**
* Reconstructs a hierarchical object from dot notation
*
* Takes a flattened object with dot notation keys and rebuilds the nested structure.
* Handles type coercion and creates intermediate objects as needed.
*
* @param flattened - Flattened object with dot notation keys
* @returns Reconstructed nested object
*
* @example
* ```typescript
* const flattened = {
* "config.database.host": "localhost",
* "config.database.port": 5432,
* "config.debug": true,
* "items": ["a", "b", "c"]
* };
*
* const result = unflattenObject(flattened);
* // {
* // config: {
* // database: {
* // host: "localhost",
* // port: 5432
* // },
* // debug: true
* // },
* // items: ["a", "b", "c"]
* // }
* ```
*/
export function unflattenObject(flattened) {
if (!flattened || typeof flattened !== 'object') {
return flattened;
}
const result = {};
for (const [key, value] of Object.entries(flattened)) {
const parts = key.split('.');
let current = result;
// Navigate/create the nested structure
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!(part in current)) {
current[part] = {};
}
else if (typeof current[part] !== 'object' || Array.isArray(current[part])) {
// If there's a conflict (primitive/array vs object), object wins
current[part] = {};
}
current = current[part];
}
// Set the final value
const finalKey = parts[parts.length - 1];
current[finalKey] = value;
}
return result;
}
/**
* Validates that flattening and unflattening are reversible
*
* Utility function for testing that ensures the flatten/unflatten operations
* preserve the original object structure (with some limitations for arrays).
*
* @param original - Original object to test
* @returns True if operations are reversible, false otherwise
*
* @example
* ```typescript
* const obj = { a: { b: { c: 1 } } };
* console.log(isReversible(obj)); // true
*
* const objWithArray = { items: [1, 2, 3], config: { debug: true } };
* console.log(isReversible(objWithArray)); // true
* ```
*/
export function isReversible(original) {
try {
const flattened = flattenObject(original);
const restored = unflattenObject(flattened);
// Deep comparison (simplified for basic types)
return JSON.stringify(original) === JSON.stringify(restored);
}
catch {
return false;
}
}
/**
* Gets all dot notation paths from an object
*
* Returns all possible dot notation paths that would be created by flattening,
* useful for validation and debugging.
*
* @param obj - Object to get paths from
* @returns Array of dot notation paths
*
* @example
* ```typescript
* const obj = {
* user: {
* profile: {
* name: "John"
* },
* settings: {
* theme: "dark"
* }
* }
* };
*
* const paths = getObjectPaths(obj);
* // ["user.profile.name", "user.settings.theme"]
* ```
*/
export function getObjectPaths(obj, prefix = '', timeoutMs = 5000) {
const flattened = flattenObject(obj, prefix, new WeakSet(), Date.now(), timeoutMs);
return Object.keys(flattened);
}
// Exported for testing - not part of public API
export { isAtomicValue as _isAtomicValue };
//# sourceMappingURL=object-flattener.js.map