UNPKG

@pho9ubenaa/remark-mask-text-beta

Version:

A remark plugin to mask text content with block characters

234 lines 9.32 kB
import { isParentNode, isTextNode } from "./ast-types.js"; import { ERROR_TYPES, FORMATTING, NODE_TYPES, NUMERIC_LIMITS, } from "./constants.js"; import { isFailure } from "./types/result.js"; import { findMaskRegionsOptimized } from "./utils.js"; /** * Creates a new text node with the specified content * * This factory function ensures consistent text node creation with proper * type safety and immutable structure. * * @param content - The text content for the node * @returns A new immutable text node */ export const createTextNode = (content) => ({ type: NODE_TYPES.TEXT, value: content, }); /** * Creates nodes from text content before a mask region * * @param text - The original text * @param start - Start position for extraction * @param end - End position for extraction * @returns Array of text nodes or empty array if no content */ const createNodesBeforeRegion = (text, start, end) => { if (end <= start) return []; const content = text.slice(start, end); return content.length > 0 ? [createTextNode(content)] : []; }; /** * Creates masked content nodes for a specific region * * @param region - The mask region to process * @param maskChar - The character to use for masking * @returns Text node containing the masked content */ const createMaskedContentNode = (region, maskChar) => { const maskedContent = maskChar.repeat(region.content.length); return createTextNode(maskedContent); }; /** * Processes mask regions and creates corresponding nodes * * This function implements the core logic for converting mask regions * into appropriate AST nodes while maintaining the original text structure. * * @param regions - Array of mask regions to process * @param originalText - The original text content * @param maskChar - The character to use for masking * @returns Array of AST nodes representing the transformed content */ const createNodesFromRegions = (regions, originalText, maskChar) => { if (regions.length === 0) { return [createTextNode(originalText)]; } const nodes = []; let lastIndex = 0; /** Process each mask region sequentially */ for (const region of regions) { /** Add text before the current mask region */ const beforeNodes = createNodesBeforeRegion(originalText, lastIndex, region.start); nodes.push(...beforeNodes); /** Add the masked content */ const maskedNode = createMaskedContentNode(region, maskChar); nodes.push(maskedNode); lastIndex = region.end; } /** Add remaining text after the last mask region */ const remainingNodes = createNodesBeforeRegion(originalText, lastIndex, originalText.length); nodes.push(...remainingNodes); return nodes; }; /** * Filters out empty text nodes from an array of nodes * * This function ensures clean AST structure by removing text nodes * with empty content, following the principle of minimal representation. * * @param nodes - Array of AST nodes to filter * @returns Filtered array with non-empty text nodes only */ const filterEmptyTextNodes = (nodes) => nodes.filter((node) => { if (isTextNode(node)) { return node.value.length > 0; } return true; }); /** * Transforms a text node containing mask delimiters into multiple nodes * * This is the main transformation function that processes individual text nodes * to identify and mask specified regions. It follows functional programming * principles with immutable transformations and explicit error handling. * * @param textNode - The text node to transform * @param maskChar - The character to use for masking * @param delimiter - The delimiter pattern to search for * @returns Result containing either transformed nodes or error information */ export const transformTextNodeFunctional = (textNode, maskChar, delimiter) => { try { /** Find mask regions in the text content */ const regionsResult = findMaskRegionsOptimized(textNode.value, delimiter); if (isFailure(regionsResult)) { return { ok: false, error: regionsResult.error, }; } const regions = regionsResult.data; /** If no mask regions found, return the original node unchanged */ if (regions.length === 0) { return { ok: true, data: [textNode] }; } /** Create nodes from the identified regions */ const transformedNodes = createNodesFromRegions(regions, textNode.value, maskChar); /** Filter out empty text nodes for clean structure */ const filteredNodes = filterEmptyTextNodes(transformedNodes); return { ok: true, data: filteredNodes }; } catch { return { ok: false, error: { type: ERROR_TYPES.AST_TRANSFORMATION_ERROR, message: "Failed to transform text node", context: `Text: "${textNode.value.substring(0, NUMERIC_LIMITS.ERROR_CONTEXT_MAX_LENGTH)}${textNode.value.length > NUMERIC_LIMITS.ERROR_CONTEXT_MAX_LENGTH ? FORMATTING.TRUNCATION_SUFFIX : ""}"`, }, }; } }; /** * Transforms an AST tree using functional programming principles * * This function implements immutable tree transformation using a bottom-up * approach that processes leaf nodes first, then builds up the transformed tree. * It maintains the original tree structure while applying transformations. * * @param node - The AST node to transform * @param maskChar - The character to use for masking * @param delimiter - The delimiter pattern to search for * @param context - Transformation context for error reporting * @returns Result containing either the transformed node or error information */ export const transformTreeFunctional = (node, maskChar, delimiter, context = { depth: 0, nodePath: [], stats: { regionsFound: 0, charactersMasked: 0, nodesProcessed: 0, processingTimeMs: 0, }, }) => { try { /** const startTime = performance.now(); */ /** Handle text nodes with potential mask content */ if (isTextNode(node) && node.value.includes(delimiter)) { const transformResult = transformTextNodeFunctional(node, maskChar, delimiter); if (isFailure(transformResult)) { return { ok: false, error: transformResult.error, }; } /** * Update statistics (currently unused but available for future enhancements) * const processingTime = performance.now() - startTime; * const updatedStats: ProcessingStats = { * ...context.stats, * nodesProcessed: context.stats.nodesProcessed + 1, * processingTimeMs: context.stats.processingTimeMs + processingTime, * }; */ /** Return array of nodes if transformation created multiple nodes */ if (transformResult.data.length === 1) { return { ok: true, data: transformResult.data[0] }; } else { return { ok: true, data: transformResult.data }; } } /** Handle parent nodes recursively */ if (isParentNode(node)) { const transformedChildren = []; for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; const childContext = { depth: context.depth + 1, nodePath: [...context.nodePath, `children[${i}]`], stats: context.stats, }; const childResult = transformTreeFunctional(child, maskChar, delimiter, childContext); if (isFailure(childResult)) { return { ok: false, error: childResult.error, }; } /** Handle cases where transformation returns multiple nodes */ if (Array.isArray(childResult.data)) { transformedChildren.push(...childResult.data); } else { transformedChildren.push(childResult.data); } } /** * Return new parent node with transformed children. * Preserve the original node type (Root or other parent types) */ const transformedParent = { ...node, children: transformedChildren.filter((child) => child !== null && child !== undefined), }; return { ok: true, data: transformedParent }; } /** Return unchanged node for non-text, non-parent nodes */ return { ok: true, data: node }; } catch { return { ok: false, error: { type: ERROR_TYPES.AST_TRANSFORMATION_ERROR, message: `Failed to transform AST node at depth ${context.depth}`, context: `Node path: ${context.nodePath.join(FORMATTING.NODE_PATH_SEPARATOR)}, Node type: ${node.type}`, }, }; } }; //# sourceMappingURL=ast-manipulator.js.map