legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
111 lines • 4.03 kB
JavaScript
/**
* Remark plugin for signature lines
*
* Detects long sequences of underscores (10 or more consecutive) and wraps them
* in HTML spans with a CSS class for styling. This is commonly used in legal
* documents to indicate signature lines.
*
* @example
* Input: "Signature: __________________________"
* Output: "Signature: <span class=\"signature-line\">__________________________</span>"
*
* @module plugins/remark/signature-lines
*/
import { visit } from 'unist-util-visit';
import { logger } from '../../utils/logger.js';
/**
* Default options for the plugin
*/
const DEFAULT_OPTIONS = {
minUnderscores: 10,
addCssClass: true,
cssClassName: 'signature-line',
debug: false,
};
/**
* Sanitize CSS class name to prevent XSS vulnerabilities
*
* Ensures the class name only contains valid CSS identifier characters:
* - Letters (a-z, A-Z)
* - Digits (0-9)
* - Hyphens (-)
* - Underscores (_)
*
* @param className - The class name to sanitize
* @returns Sanitized class name, or default if invalid
*/
function sanitizeCssClassName(className) {
// Valid CSS identifier pattern: starts with letter/underscore, followed by letters/digits/hyphens/underscores
const validPattern = /^[a-zA-Z_][\w-]*$/;
if (!className || !validPattern.test(className)) {
logger.warn(`Invalid CSS class name "${className}", using default "signature-line"`);
return 'signature-line';
}
return className;
}
/**
* Remark plugin that detects and marks signature lines
*
* This plugin processes text nodes in the markdown AST and identifies sequences
* of underscores that are likely signature lines (by default, 10 or more consecutive
* underscores). When found, it wraps them in HTML span elements with a CSS class
* for styling.
*
* @param options - Configuration options for the plugin
* @returns A unified transformer function
*
* @example
* ```typescript
* unified()
* .use(remarkParse)
* .use(remarkSignatureLines, { minUnderscores: 15 })
* .use(remarkStringify)
* ```
*/
const remarkSignatureLines = (options = {}) => {
const config = { ...DEFAULT_OPTIONS, ...options };
// Sanitize CSS class name to prevent XSS
const safeCssClassName = sanitizeCssClassName(config.cssClassName);
return (tree) => {
if (config.debug) {
logger.debug('Processing tree...');
}
visit(tree, 'text', (node, index, parent) => {
if (index === undefined || !parent)
return;
const text = node.value;
// Create regex pattern for detecting long underscore sequences
const underscorePattern = new RegExp(`_{${config.minUnderscores},}`, 'g');
// Check if text contains signature lines
if (!underscorePattern.test(text)) {
return; // No signature lines found, skip this node
}
if (config.debug) {
logger.debug('Found signature line in text:', text);
}
// If we're not adding CSS classes, leave the text as-is
if (!config.addCssClass) {
return;
}
// Process the text and wrap signature lines in HTML spans
const processedText = text.replace(underscorePattern, match => {
if (config.debug) {
logger.debug(`Wrapping ${match.length} underscores`);
}
return `<span class="${safeCssClassName}">${match}</span>`;
});
// Replace the text node with an HTML node containing the wrapped signature lines
const htmlNode = {
type: 'html',
value: processedText,
};
// Replace the text node with the new HTML node
parent.children[index] = htmlNode;
});
if (config.debug) {
logger.debug('Processing complete');
}
};
};
export default remarkSignatureLines;
//# sourceMappingURL=signature-lines.js.map