UNPKG

@pho9ubenaa/remark-mask-text-beta

Version:

A remark plugin to mask text content with block characters

262 lines 9.67 kB
/** * Utility functions for remark-mask-text plugin * * This module provides pure helper functions for text processing, input validation, * and pattern matching operations. All functions follow functional programming * principles with immutable data structures and explicit error handling. * * Why (Business Logic Background): * - Input validation: To safely process user-provided configuration values and prevent security vulnerabilities * - Performance optimization: To improve processing speed by avoiding regex recompilation * - Error recovery: To provide appropriate error messages for invalid input and support debugging * - Consistency guarantee: To ensure result consistency by using the same algorithm for all text processing * - Extensibility: To facilitate support for new delimiters and special characters */ import { DEFAULT_MASK_CHARACTER, DEFAULT_MASK_DELIMITER, ERROR_DETAILS, ERROR_MESSAGES, ERROR_TYPES, MAX_DELIMITER_LENGTH, NUMERIC_LIMITS, REGEX_FLAGS, } from "./constants.js"; /** Re-export type guards from ast-types for backward compatibility */ export { isParentNode as isParent, isTextNode } from "./ast-types.js"; /** * Creates a regex factory function for better performance * * This approach avoids creating new RegExp objects on every call and * handles the global flag properly to prevent state issues. * * @param delimiter - The delimiter to create regex for * @returns A function that creates fresh regex instances */ const createRegexFactory = (delimiter) => { const escapedDelimiter = escapeRegExp(delimiter); const pattern = `${escapedDelimiter}(.*?)${escapedDelimiter}`; return () => new RegExp(pattern, REGEX_FLAGS.GLOBAL); }; /** * Escapes special regular expression characters in a string * * This function is essential for safely using user-provided delimiters * in regular expressions without unintended pattern matching. It handles * all special regex metacharacters to ensure literal matching. * * @param text - The string to escape * @returns The escaped string safe for use in RegExp * * @example * escapeRegExp('||') // Returns '\\|\\|' * escapeRegExp('::') // Returns '::' * escapeRegExp('.*') // Returns '\\.\\*' */ export const escapeRegExp = (text) => { return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }; /** * Validates a delimiter string for mask processing * * Ensures the delimiter meets requirements for safe and effective processing. * The delimiter must be non-empty, not contain only whitespace, and not be * excessively long to prevent performance issues. * * @param delimiter - The delimiter to validate * @returns Result containing valid delimiter or validation error */ export const validateDelimiter = (delimiter) => { if (!delimiter) { return { ok: false, error: { type: ERROR_TYPES.INVALID_DELIMITER, message: ERROR_MESSAGES.DELIMITER_EMPTY, detail: ERROR_DETAILS.DELIMITER_REQUIRED, }, }; } if (delimiter.trim().length === 0) { return { ok: false, error: { type: ERROR_TYPES.INVALID_DELIMITER, message: ERROR_MESSAGES.DELIMITER_WHITESPACE, detail: ERROR_DETAILS.DELIMITER_WHITESPACE_ISSUE, }, }; } if (delimiter.length > MAX_DELIMITER_LENGTH) { return { ok: false, error: { type: ERROR_TYPES.INVALID_DELIMITER, message: ERROR_MESSAGES.DELIMITER_TOO_LONG, detail: `Delimiter "${delimiter}" exceeds maximum length of ${MAX_DELIMITER_LENGTH} characters`, }, }; } return { ok: true, data: delimiter }; }; /** * Validates a mask character for content replacement * * Ensures the mask character is suitable for replacing masked content. * Must be exactly one character and not a whitespace character. * * @param maskChar - The mask character to validate * @returns Result containing valid mask character or validation error */ export const validateMaskCharacter = (maskChar) => { if (!maskChar) { return { ok: false, error: { type: ERROR_TYPES.INVALID_MASK_CHARACTER, message: ERROR_MESSAGES.MASK_CHAR_EMPTY, detail: ERROR_DETAILS.MASK_CHAR_REQUIRED, }, }; } if (maskChar.length !== 1) { return { ok: false, error: { type: ERROR_TYPES.INVALID_MASK_CHARACTER, message: ERROR_MESSAGES.MASK_CHAR_MULTIPLE, detail: `Provided mask character "${maskChar}" has length ${maskChar.length}`, }, }; } if (/\s/.test(maskChar)) { return { ok: false, error: { type: ERROR_TYPES.INVALID_MASK_CHARACTER, message: ERROR_MESSAGES.MASK_CHAR_WHITESPACE, detail: ERROR_DETAILS.MASK_CHAR_WHITESPACE_ISSUE, }, }; } return { ok: true, data: maskChar }; }; /** * Validates and normalizes plugin options * * Takes user-provided options and validates them, providing defaults for * missing values. This ensures all subsequent processing operates on * guaranteed-valid configuration. * * @param options - The user-provided options to validate * @returns Result containing validated options or validation errors */ export const validateOptions = (options) => { const maskChar = options.maskCharacter ?? DEFAULT_MASK_CHARACTER; const delimiter = options.maskDelimiter ?? DEFAULT_MASK_DELIMITER; /** Validate mask character */ const maskCharResult = validateMaskCharacter(maskChar); if (!maskCharResult.ok) { return maskCharResult; } /** Validate delimiter */ const delimiterResult = validateDelimiter(delimiter); if (!delimiterResult.ok) { return delimiterResult; } return { ok: true, data: { maskCharacter: maskCharResult.data, maskDelimiter: delimiterResult.data, }, }; }; /** * Optimized function to find all mask regions in text * * Uses String.prototype.matchAll for better performance compared to * exec() loops. Handles nested delimiters by processing outermost pairs first. * This approach provides linear time complexity and better memory efficiency. * * @param text - The text to search for mask regions * @param delimiter - The delimiter pattern to search for * @returns Result containing mask regions or processing error * * @example * findMaskRegionsOptimized('This is ::secret:: text', '::') * // Returns { ok: true, data: [{ start: 8, end: 18, content: 'secret' }] } */ export const findMaskRegionsOptimized = (text, delimiter) => { try { /** Validate inputs */ const delimiterResult = validateDelimiter(delimiter); if (!delimiterResult.ok) { return delimiterResult; } const regexFactory = createRegexFactory(delimiter); const regex = regexFactory(); /** Use matchAll for better performance and cleaner code */ const matches = Array.from(text.matchAll(regex)); const regions = matches.map((match) => ({ start: match.index ?? 0, end: (match.index ?? 0) + match[0].length, content: match[1], })); return { ok: true, data: regions }; } catch { return { ok: false, error: { type: ERROR_TYPES.REGEX_PROCESSING_ERROR, message: "Failed to process mask regions", pattern: delimiter, }, }; } }; /** * Legacy function for backward compatibility * * Maintains the original API while delegating to the optimized implementation. * This function will be deprecated in future versions. * * @deprecated Use findMaskRegionsOptimized instead * @param text - The text to search for mask regions * @param delimiter - The delimiter pattern to search for * @returns Array of mask regions found in the text */ export const findMaskRegions = (text, delimiter) => { const result = findMaskRegionsOptimized(text, delimiter); return result.ok ? Array.from(result.data) : []; }; /** * Functional composition utility for chaining operations * * Enables elegant composition of processing functions following * functional programming principles. * * @param fns - Array of functions to compose * @returns Composed function that applies all functions in sequence */ export const pipe = (...fns) => (value) => fns.reduce((acc, fn) => fn(acc), value); /** * Creates a memoized version of a function for performance optimization * * Useful for caching expensive computations like regex compilation. * Uses a simple Map-based cache with size limits to prevent memory leaks. * * @param fn - The function to memoize * @param maxCacheSize - Maximum number of cached results * @returns Memoized version of the function */ export const memoize = (fn, maxCacheSize = NUMERIC_LIMITS.DEFAULT_CACHE_SIZE) => { const cache = new Map(); return (...args) => { const key = JSON.stringify(args); const cachedValue = cache.get(key); if (cachedValue !== undefined) { return cachedValue; } const result = fn(...args); /** Implement simple LRU by clearing cache when it gets too large */ if (cache.size >= maxCacheSize) { cache.clear(); } cache.set(key, result); return result; }; }; //# sourceMappingURL=utils.js.map