legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
264 lines • 10.2 kB
JavaScript
/**
* YAML Front Matter Auto-Population Module for Legal Markdown Documents
*
* This module provides functionality to auto-populate YAML front matter with inferred
* header level patterns and processing properties following the original Legal Markdown
* specification. It analyzes document structure and generates enhanced metadata.
*
* Features:
* - Document structure analysis for header level inference
* - Auto-population of missing header level definitions
* - Properties section generation (no-indent, no-reset, level-style)
* - YAML front matter enhancement and standardization
* - Support for both traditional (l., ll., lll.) and alternative syntax
*
* @example
* ```typescript
* import { autoPopulateYamlFrontMatter } from './yaml-auto-population.js';
*
* const content = `---
* level-1: "Article 1."
* level-2: "Section 1."
* ---
* l. Introduction
* ll. Terms`;
*
* const enhanced = autoPopulateYamlFrontMatter(content);
* // Returns document with enhanced YAML front matter including
* // all level definitions and Properties section
* ```
*
* @module
*/
import { parseYamlFrontMatter } from '../parsers/yaml-parser.js';
import { DEFAULT_HEADER_PATTERNS, DEFAULT_PROPERTIES } from '../../constants/headers.js';
/**
* Default header level patterns following Legal Markdown conventions
*
* For complete documentation on header formats and variable usage, see:
* @see {@link ../../../docs/headers_numbering.md} - Complete guide to headers and numbering system
*/
/**
* Auto-populates YAML front matter with inferred header patterns and properties
*
* Analyzes the document structure and enhances the YAML front matter with:
* - Missing header level definitions
* - Properties section with processing configurations
* - Properly quoted level definitions
*
* @param {string} content - The document content with YAML front matter
* @param {YamlAutoPopulationOptions} [options={}] - Configuration options
* @returns {string} Document with enhanced YAML front matter
* @example
* ```typescript
* const input = `---
* level-1: "Article 1."
* ---
* l. Introduction
* ll. Terms`;
*
* const result = autoPopulateYamlFrontMatter(input);
* // Returns document with complete level definitions and Properties section
* ```
*/
export function autoPopulateYamlFrontMatter(content, options = {}) {
const { includeProperties = true, inferMissingLevels = true, ensureQuotedLevels = true, } = options;
// Parse existing YAML front matter
const parsed = parseYamlFrontMatter(content);
if (!content.startsWith('---')) {
// No YAML front matter exists, create minimal structure
const enhancedMetadata = createEnhancedMetadata({}, includeProperties, inferMissingLevels);
const yamlString = formatEnhancedYaml(enhancedMetadata);
return `---\n${yamlString}---\n\n${content}`;
}
// Enhance existing metadata
const enhancedMetadata = createEnhancedMetadata(parsed.metadata, includeProperties, inferMissingLevels, ensureQuotedLevels);
// Serialize enhanced metadata with custom formatting
const yamlString = formatEnhancedYaml(enhancedMetadata);
return `---\n${yamlString}---\n\n${parsed.content}`;
}
/**
* Creates enhanced metadata with structured headers and properties sections
*
* @private
* @param {Record<string, any>} existingMetadata - Current document metadata
* @param {boolean} includeProperties - Whether to include Properties section
* @param {boolean} inferMissingLevels - Whether to infer missing levels
* @param {boolean} ensureQuotedLevels - Whether to ensure levels are quoted
* @returns {Record<string, any>} Enhanced metadata object
*/
function createEnhancedMetadata(existingMetadata, includeProperties = true, inferMissingLevels = true, _ensureQuotedLevels = true) {
const enhanced = {};
// Add comment for Structured Headers section
enhanced['# Structured Headers'] = null;
// Process header levels (level-1 through level-9)
for (let i = 1; i <= 9; i++) {
const levelKey = `level-${i}`;
let levelValue = existingMetadata[levelKey];
if (!levelValue && inferMissingLevels) {
// Use default pattern if missing
levelValue = DEFAULT_HEADER_PATTERNS[levelKey];
}
if (levelValue) {
// Store the value as-is, formatting will handle quoting
enhanced[levelKey] = levelValue;
}
}
// Add other existing metadata (excluding level-* keys)
for (const [key, value] of Object.entries(existingMetadata)) {
if (!key.startsWith('level-') && !key.startsWith('#')) {
enhanced[key] = value;
}
}
// Add Properties section if requested
if (includeProperties) {
enhanced['# Properties'] = null;
// Add default properties if not already present
for (const [propKey, defaultValue] of Object.entries(DEFAULT_PROPERTIES)) {
if (!(propKey in existingMetadata)) {
enhanced[propKey] = defaultValue;
}
else {
enhanced[propKey] = existingMetadata[propKey];
}
}
}
return enhanced;
}
/**
* Analyzes document content to infer header usage patterns
*
* Scans the document content to determine which header levels are actually used
* and what no-indent patterns might be appropriate.
*
* @private
* @param {string} content - The document content to analyze
* @returns {Object} Analysis results with detected patterns
*/
function analyzeDocumentStructure(content) {
const lines = content.split('\n');
const usedLevels = new Set();
const noIndentLines = [];
let hasAlternativeSyntax = false;
for (const line of lines) {
const trimmedLine = line.trim();
// Check for traditional syntax (l., ll., lll., etc.)
const traditionalMatch = trimmedLine.match(/^(l+)\.\s/);
if (traditionalMatch) {
const level = traditionalMatch[1].length;
usedLevels.add(level);
noIndentLines.push(traditionalMatch[1] + '.');
}
// Check for alternative syntax (l2., l3., etc.)
const alternativeMatch = trimmedLine.match(/^l(\d+)\.\s/);
if (alternativeMatch) {
const level = parseInt(alternativeMatch[1], 10);
usedLevels.add(level);
hasAlternativeSyntax = true;
noIndentLines.push(`l${level}.`);
}
}
return {
usedLevels: Array.from(usedLevels).sort((a, b) => a - b),
noIndentPattern: noIndentLines.length > 0 ? noIndentLines.join(', ') : '',
hasAlternativeSyntax,
};
}
/**
* Generates appropriate no-indent pattern based on document analysis
*
* @private
* @param {string} content - Document content to analyze
* @returns {string} Comma-separated no-indent pattern
*/
export function generateNoIndentPattern(content) {
const analysis = analyzeDocumentStructure(content);
return analysis.noIndentPattern;
}
/**
* Validates and normalizes YAML front matter structure
*
* Ensures the YAML front matter follows the expected structure with
* proper sectioning and formatting.
*
* @param {Record<string, any>} metadata - Metadata to validate
* @returns {Record<string, any>} Normalized metadata
*/
export function normalizeYamlStructure(metadata) {
const normalized = {};
// Ensure Structured Headers section comes first
normalized['# Structured Headers'] = null;
// Add level definitions in order
for (let i = 1; i <= 9; i++) {
const levelKey = `level-${i}`;
if (metadata[levelKey]) {
normalized[levelKey] = metadata[levelKey];
}
}
// Add other metadata (excluding comments and level keys)
for (const [key, value] of Object.entries(metadata)) {
if (!key.startsWith('level-') && !key.startsWith('#')) {
normalized[key] = value;
}
}
// Add Properties section
if (metadata['no-indent'] !== undefined ||
metadata['no-reset'] !== undefined ||
metadata['level-style'] !== undefined) {
normalized['# Properties'] = null;
if (metadata['no-indent'] !== undefined) {
normalized['no-indent'] = metadata['no-indent'];
}
if (metadata['no-reset'] !== undefined) {
normalized['no-reset'] = metadata['no-reset'];
}
if (metadata['level-style'] !== undefined) {
normalized['level-style'] = metadata['level-style'];
}
}
return normalized;
}
/**
* Formats enhanced metadata as YAML with proper comment handling
*
* @private
* @param {Record<string, any>} metadata - Enhanced metadata to format
* @returns {string} Formatted YAML string
*/
function formatEnhancedYaml(metadata) {
const lines = [];
for (const [key, value] of Object.entries(metadata)) {
if (key.startsWith('#')) {
// Add comment line
lines.push(key);
}
else if (value === null || value === undefined) {
// Skip null values (used for comments)
continue;
}
else if (typeof value === 'string') {
// Handle string values, preserving existing quotes or adding them
let formattedValue = value;
if (value === '') {
formattedValue = '""';
}
else if (!value.startsWith('"') || !value.endsWith('"')) {
// Only add quotes if not already quoted
formattedValue = `"${value}"`;
}
lines.push(`${key}: ${formattedValue}`);
}
else if (key.startsWith('level-')) {
// For level keys, always treat as quoted strings even if they're numbers
lines.push(`${key}: "${value}"`);
}
else {
// Handle other values
lines.push(`${key}: ${JSON.stringify(value)}`);
}
}
return lines.join('\n') + '\n';
}
// Exported for testing - not part of public API
export { createEnhancedMetadata as _createEnhancedMetadata, analyzeDocumentStructure as _analyzeDocumentStructure, formatEnhancedYaml as _formatEnhancedYaml, };
//# sourceMappingURL=yaml-auto-population.js.map