legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
260 lines • 9.78 kB
JavaScript
/**
* @fileoverview reStructuredText Parser Extension for Legal Markdown
*
* This module provides functionality to parse reStructuredText (.rst) documents
* and convert them to Legal Markdown format. It uses Pandoc for robust parsing
* with intelligent fallback to basic regex-based parsing when Pandoc is not
* available or fails.
*
* Features:
* - AST-based reStructuredText parsing using Pandoc
* - Fallback regex-based parsing for simple documents
* - Content type detection and validation
* - Section header conversion to Legal Markdown syntax
* - Text formatting preservation (emphasis, strong)
* - Code block and literal block handling
* - Bullet and enumerated list conversion
* - Reference and hyperlink preservation
*
* @example
* ```typescript
* import { convertRstToLegalMarkdown, convertRstToLegalMarkdownSync } from './rst-parser.js';
*
* const rstContent = `
* ============
* Introduction
* ============
*
* This is a sample reStructuredText document.
*
* Section 1
* =========
*
* Some *emphasized* and **strong** text.
* `;
*
* // Async conversion (preferred)
* const legalMarkdown = await convertRstToLegalMarkdown(rstContent);
*
* // Sync conversion (fallback)
* const legalMarkdownSync = convertRstToLegalMarkdownSync(rstContent);
* ```
*/
import { ContentDetector } from './parsers/content-detector.js';
import { PandocFactory } from './parsers/pandoc-factory.js';
import { FallbackParsers } from './parsers/fallback-parsers.js';
/**
* Convert RST AST node to markdown
*/
function astToMarkdown(node, level = 0) {
if (!node)
return '';
switch (node.type) {
case 'document':
return node.children?.map((child) => astToMarkdown(child, level)).join('\n\n') || '';
case 'section': {
const headerLevel = Math.min(level + 1, 5);
const headerPrefix = 'l'.repeat(headerLevel);
const title = node.children?.find((child) => child.type === 'title');
const titleText = title ? astToMarkdown(title, level) : '';
const content = node.children
?.filter((child) => child.type !== 'title')
.map((child) => astToMarkdown(child, level + 1))
.join('\n\n');
return `${headerPrefix}. ${titleText}\n\n${content}`.trim();
}
case 'title':
return node.children?.map((child) => astToMarkdown(child, level)).join('') || '';
case 'paragraph':
return node.children?.map((child) => astToMarkdown(child, level)).join('') || '';
case 'text':
return node.value || '';
case 'emphasis': {
const emContent = node.children?.map((child) => astToMarkdown(child, level)).join('') || '';
return `*${emContent}*`;
}
case 'strong': {
const strongContent = node.children?.map((child) => astToMarkdown(child, level)).join('') || '';
return `**${strongContent}**`;
}
case 'literal':
return `\`${node.value || ''}\``;
case 'literal_block': {
const code = node.value ||
node.children?.map((child) => astToMarkdown(child, level)).join('') ||
'';
return '```\n' + code + '\n```';
}
case 'bullet_list':
return node.children?.map((child) => astToMarkdown(child, level)).join('\n') || '';
case 'enumerated_list': {
let counter = 1;
return (node.children
?.map((child) => {
const content = astToMarkdown(child, level);
return content.replace(/^/, `${counter++}. `);
})
.join('\n') || '');
}
case 'list_item': {
const itemContent = node.children?.map((child) => astToMarkdown(child, level)).join('\n') || '';
// Handle bullet or enumerated list items
return node.parent?.type === 'bullet_list' ? `- ${itemContent}` : itemContent;
}
case 'reference': {
const refText = node.children?.map((child) => astToMarkdown(child, level)).join('') || '';
const refUri = node.refuri || node.refname || '';
return refUri ? `[${refText}](${refUri})` : refText;
}
case 'target':
// Targets are usually invisible in output
return '';
case 'block_quote': {
const quoteContent = node.children?.map((child) => astToMarkdown(child, level)).join('\n') || '';
return quoteContent
.split('\n')
.map((line) => `> ${line}`)
.join('\n');
}
case 'note':
case 'warning':
case 'important': {
const admonitionType = node.type.charAt(0).toUpperCase() + node.type.slice(1);
const admonitionContent = node.children?.map((child) => astToMarkdown(child, level)).join('\n') || '';
return `> **${admonitionType}:** ${admonitionContent}`;
}
case 'definition_list':
return node.children?.map((child) => astToMarkdown(child, level)).join('\n\n') || '';
case 'definition_list_item': {
const term = node.children?.find((child) => child.type === 'term');
const definition = node.children?.find((child) => child.type === 'definition');
const termText = term ? astToMarkdown(term, level) : '';
const defText = definition ? astToMarkdown(definition, level) : '';
return `**${termText}**\n ${defText}`;
}
case 'term':
case 'definition':
return node.children?.map((child) => astToMarkdown(child, level)).join('') || '';
case 'footnote_reference':
return `[^${node.refname}]`;
case 'footnote': {
const footnoteNum = node.names?.[0] || '';
const footnoteContent = node.children?.map((child) => astToMarkdown(child, level)).join(' ') || '';
return `[^${footnoteNum}]: ${footnoteContent}`;
}
case 'substitution_reference':
return node.refname || '';
case 'inline':
return node.children?.map((child) => astToMarkdown(child, level)).join('') || '';
default:
// For unknown node types, try to extract text from children
if (node.children) {
return node.children.map((child) => astToMarkdown(child, level)).join('');
}
return node.value || '';
}
}
/**
* Converts reStructuredText content to markdown format using pandoc
*/
export async function parseRestructuredText(content) {
try {
const parser = await PandocFactory.createForContent(content);
if (parser) {
const markdown = await parser.convert(content, 'rst', 'markdown');
return markdown.replace(/\n{3,}/g, '\n\n').trim();
}
else {
// Use fallback parser
return FallbackParsers.convertRstBasic(content);
}
}
catch (error) {
if (process.env.NODE_ENV !== 'test') {
console.error('Error parsing RST:', error);
}
// Use fallback parser on error
return FallbackParsers.convertRstBasic(content);
}
}
/**
* Synchronous version using fallback parser
*/
export function parseRestructuredTextSync(content) {
return FallbackParsers.convertRstBasic(content);
}
/**
* Checks if a file appears to be reStructuredText
*/
export function isRestructuredText(content) {
return ContentDetector.needsRstParser(content);
}
/**
* Converts reStructuredText content to Legal Markdown format (async version)
*
* This function provides robust conversion of reStructuredText documents to Legal
* Markdown format using Pandoc when available. It automatically detects RST content
* and only performs conversion when necessary, preserving non-RST content unchanged.
*
* @param {string} content - The input content that may contain reStructuredText
* @returns {Promise<string>} A promise that resolves to Legal Markdown formatted content
* @example
* ```typescript
* const rstContent = `
* ============
* Introduction
* ============
*
* This is a sample reStructuredText document.
* `;
*
* const legalMarkdown = await convertRstToLegalMarkdown(rstContent);
* console.log(legalMarkdown);
* // Output:
* // l. Introduction
* //
* // This is a sample reStructuredText document.
* ```
*/
export async function convertRstToLegalMarkdown(content) {
// Only convert if it looks like RST
if (!isRestructuredText(content)) {
return content;
}
return await parseRestructuredText(content);
}
/**
* Converts reStructuredText content to Legal Markdown format (sync version)
*
* This synchronous function provides fallback conversion of reStructuredText
* documents using regex-based parsing when Pandoc is not available. It's designed
* for environments where async operations are not suitable or when simple
* conversion is sufficient.
*
* @param {string} content - The input content that may contain reStructuredText
* @returns {string} Legal Markdown formatted content
* @example
* ```typescript
* const rstContent = `
* Section Title
* =============
*
* Some **bold** and *italic* text.
* `;
*
* const legalMarkdown = convertRstToLegalMarkdownSync(rstContent);
* console.log(legalMarkdown);
* // Output:
* // l. Section Title
* //
* // Some **bold** and *italic* text.
* ```
*/
export function convertRstToLegalMarkdownSync(content) {
// Only convert if it looks like RST
if (!isRestructuredText(content)) {
return content;
}
return parseRestructuredTextSync(content);
}
//# sourceMappingURL=rst-parser.js.map