@pho9ubenaa/remark-mask-text-beta
Version:
A remark plugin to mask text content with block characters
262 lines • 9.67 kB
JavaScript
/**
* 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