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