legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
168 lines • 5.89 kB
JavaScript
/**
* Remark plugin for field tracking in Legal Markdown documents
*
* This plugin processes text nodes to track field usage for highlighting and
* analysis purposes. It excludes code blocks and inline code to prevent
* false positives from field-like patterns in code.
*
* Features:
* - Tracks field patterns like {{field_name}} in text nodes only
* - Excludes code and inlineCode nodes to prevent contamination
* - Integrates with field tracker for highlighting support
* - Supports both simple fields and complex field expressions
* - Maintains field usage statistics and metadata
*
* @example
* ```typescript
* import { unified } from 'unified';
* import remarkParse from 'remark-parse';
* import remarkFieldTracking from './field-tracking.js';
*
* const processor = unified()
* .use(remarkParse)
* .use(remarkFieldTracking, {
* patterns: ['{{(.+?)}}'], // Custom field patterns
* trackingEnabled: true
* });
* ```
*
* @module
*/
import { visit } from 'unist-util-visit';
import { fieldTracker } from '../../extensions/tracking/field-tracker.js';
/**
* Default field patterns to track
*/
const DEFAULT_FIELD_PATTERNS = [
'{{(.+?)}}', // Standard Legal Markdown fields: {{field_name}}
];
/**
* Check if a node should be excluded from field tracking
*/
function shouldExcludeNode(node, parent) {
// Exclude code blocks and inline code
if (node.type === 'code' || node.type === 'inlineCode') {
return true;
}
// Exclude text nodes that are children of code elements
if (parent && (parent.type === 'code' || parent.type === 'inlineCode')) {
return true;
}
// Allow all other text nodes
return false;
}
/**
* Extract field name from matched pattern
*/
function extractFieldName(match, pattern) {
const regex = new RegExp(pattern);
const result = regex.exec(match);
return result && result[1] ? result[1].trim() : match;
}
/**
* Resolve field value from metadata or return empty string
*/
function resolveFieldValue(fieldName, metadata) {
// Try direct metadata lookup
if (metadata[fieldName] !== undefined) {
return String(metadata[fieldName]);
}
// Try nested lookup for dotted field names (e.g., 'contract.date')
const keys = fieldName.split('.');
let current = metadata;
for (const key of keys) {
if (current && typeof current === 'object' && key in current) {
current = current[key];
}
else {
return ''; // Field not found
}
}
return current !== undefined ? String(current) : '';
}
/**
* Track fields found in a text node
*/
function trackFieldsInTextNode(textNode, patterns, metadata, debug = false) {
const results = [];
const content = textNode.value;
for (const pattern of patterns) {
const regex = new RegExp(pattern, 'g');
let match;
while ((match = regex.exec(content)) !== null) {
const originalValue = match[0];
const fieldName = extractFieldName(originalValue, pattern);
const resolvedValue = resolveFieldValue(fieldName, metadata);
// Track the field with the field tracker
fieldTracker.trackField(fieldName, {
value: resolvedValue,
originalValue,
hasLogic: false, // Field tracking plugin tracks simple fields
});
// Create tracking result
const result = {
key: fieldName,
originalValue,
value: resolvedValue,
hasLogic: false,
position: textNode.position
? {
line: textNode.position.start.line,
column: textNode.position.start.column,
}
: undefined,
};
results.push(result);
if (debug) {
console.log(`📋 Tracked field: ${fieldName} = "${resolvedValue}" (${originalValue})`);
}
}
}
return results;
}
/**
* Remark plugin for field tracking in Legal Markdown documents
*/
const remarkFieldTracking = (options = {}) => {
const { trackingEnabled = true, patterns = DEFAULT_FIELD_PATTERNS, debug = false, metadata = {}, } = options;
return (tree) => {
if (!trackingEnabled) {
if (debug) {
console.log('📋 Field tracking is disabled');
}
return;
}
if (debug) {
console.log('📋 Processing field tracking with remark plugin');
console.log(`📋 Using patterns: ${patterns.join(', ')}`);
}
let totalFieldsTracked = 0;
const trackedFields = new Set();
// Visit all text nodes, excluding code blocks
visit(tree, 'text', (node, index, parent) => {
// Skip nodes that should be excluded
if (shouldExcludeNode(node, parent)) {
return;
}
// Track fields in this text node
const results = trackFieldsInTextNode(node, patterns, metadata, debug);
// Update statistics
totalFieldsTracked += results.length;
results.forEach(result => trackedFields.add(result.key));
});
// Store tracking statistics in metadata
if (metadata) {
metadata['_field_tracking_stats'] = {
totalFieldsTracked,
uniqueFieldsTracked: trackedFields.size,
trackedFieldNames: Array.from(trackedFields),
patterns: patterns,
};
}
if (debug) {
console.log(`📋 Field tracking completed: ${totalFieldsTracked} fields tracked (${trackedFields.size} unique)`);
}
};
};
export default remarkFieldTracking;
//# sourceMappingURL=field-tracking.js.map