UNPKG

string-masking

Version:

Mask strings while optionally preserving formatting characters

330 lines (264 loc) 8.1 kB
const NOTE = "Please email us on rohansolse@gmail.com for NPM related suggestions/bugs (with input)."; function buildFailure(message) { return { status: "failure", response: message, NOTE, }; } function buildSuccess(response) { return { status: "success", response, }; } function normalizeInput(value) { if (value === undefined || value === null) { return null; } if (typeof value === "string") { return value; } if ( typeof value === "number" || typeof value === "bigint" || typeof value === "boolean" ) { return String(value); } return null; } function parseDigit(value) { if (typeof value === "number" && Number.isInteger(value)) { return value; } if (typeof value === "string" && value.trim() !== "") { const parsed = Number(value); if (Number.isInteger(parsed)) { return parsed; } } return null; } function repeatMask(maskChar, count) { return maskChar.repeat(Math.max(count, 0)); } function getMaskableIndexes(string, preserveFormat) { if (!preserveFormat) { return Array.from({ length: string.length }, (_, index) => index); } const maskableIndexes = []; for (let index = 0; index < string.length; index += 1) { if (isMaskableCharacter(string[index])) { maskableIndexes.push(index); } } return maskableIndexes; } function applyMaskToIndexes(string, maskableIndexes, options) { const { visibleStart = 0, visibleEnd = 0, maskChar = "X", alternateMask = false, } = options; if (maskableIndexes.length === 0) { return buildFailure("Entered string does not contain maskable characters"); } const safeVisibleStart = Math.min(visibleStart, maskableIndexes.length); const safeVisibleEnd = Math.min( visibleEnd, Math.max(maskableIndexes.length - safeVisibleStart, 0) ); const visibleStartIndexes = new Set( maskableIndexes.slice(0, safeVisibleStart) ); const visibleEndIndexes = new Set( safeVisibleEnd === 0 ? [] : maskableIndexes.slice(-safeVisibleEnd) ); const indexesToMask = maskableIndexes.filter( (index) => !visibleStartIndexes.has(index) && !visibleEndIndexes.has(index) ); if (indexesToMask.length === 0) { return buildSuccess(string); } const maskedCharacters = string.split(""); for (let index = 0; index < indexesToMask.length; index += 1) { if (alternateMask && index % 2 === 1) { continue; } maskedCharacters[indexesToMask[index]] = maskChar; } return buildSuccess(maskedCharacters.join("")); } function legacyMiddleMask(string, maskChar) { if (string.length < 3) { return buildFailure("Entered strings length is too less for this operation."); } if (string.length === 3) { return buildSuccess(string.charAt(0) + maskChar + string.charAt(2)); } const visibleEachSide = Math.max(1, Math.floor((string.length / 35) * 10)); const middleMaskCount = string.length - visibleEachSide * 2; const prefix = string.slice(0, visibleEachSide); const suffix = string.slice(string.length - visibleEachSide); return buildSuccess(prefix + repeatMask(maskChar, middleMaskCount) + suffix); } function legacyMask(string, digit, options) { const maskChar = options.maskChar; if (digit === 0) { if (options.alternateMask) { const visibleEachSide = Math.max(1, Math.floor((string.length / 35) * 10)); return formatAwareMask(string, { visibleStart: visibleEachSide, visibleEnd: visibleEachSide, preserveFormat: options.preserveFormat, maskChar, alternateMask: true, }); } return legacyMiddleMask(string, maskChar); } if (digit > 0) { if (string.length <= digit) { return buildFailure( "Entered string length cannot be equal to greater than the digit count" ); } const maskedLength = string.length - digit; const suffix = string.slice(maskedLength); if (suffix.includes(" ")) { return buildFailure("unmasking value can not be blank"); } if (options.preserveFormat) { return formatAwareMask(string, { visibleStart: 0, visibleEnd: digit, preserveFormat: true, maskChar, alternateMask: options.alternateMask, }); } if (options.alternateMask) { return formatAwareMask(string, { visibleStart: 0, visibleEnd: digit, preserveFormat: false, maskChar, alternateMask: true, }); } return buildSuccess(repeatMask(maskChar, maskedLength) + suffix); } const visibleStart = Math.abs(digit); if (string.length <= visibleStart) { return buildFailure( "Entered string length cannot be equal to greater than the digit count" ); } if (options.preserveFormat) { return formatAwareMask(string, { visibleStart, visibleEnd: 0, preserveFormat: true, maskChar, alternateMask: options.alternateMask, }); } if (options.alternateMask) { return formatAwareMask(string, { visibleStart, visibleEnd: 0, preserveFormat: false, maskChar, alternateMask: true, }); } return buildSuccess( string.slice(0, visibleStart) + repeatMask(maskChar, string.length - visibleStart) ); } function isMaskableCharacter(character) { return /[A-Za-z0-9]/.test(character); } function formatAwareMask(string, options) { const { preserveFormat = false, } = options; const maskableIndexes = getMaskableIndexes(string, preserveFormat); return applyMaskToIndexes(string, maskableIndexes, options); } function normalizeOptions(options) { if (!options || typeof options !== "object" || Array.isArray(options)) { return buildFailure("Options must be a valid object"); } const normalized = { visibleStart: options.visibleStart ?? 0, visibleEnd: options.visibleEnd ?? 0, preserveFormat: Boolean(options.preserveFormat), maskChar: options.maskChar ?? "X", alternateMask: Boolean(options.alternateMask), }; if (!Number.isInteger(normalized.visibleStart) || normalized.visibleStart < 0) { return buildFailure("visibleStart must be a non-negative integer"); } if (!Number.isInteger(normalized.visibleEnd) || normalized.visibleEnd < 0) { return buildFailure("visibleEnd must be a non-negative integer"); } if ( typeof normalized.maskChar !== "string" || normalized.maskChar.length !== 1 ) { return buildFailure("maskChar must be a single character string"); } return normalized; } function stringMask(value, digitOrOptions, maybeOptions) { try { const string = normalizeInput(value); if (string === null || digitOrOptions === undefined) { return buildFailure( "Ethier string, digit or both are missing or misplaced." ); } if (string === "") { return buildFailure("Blank string cannot be processed"); } if ( typeof digitOrOptions === "object" && digitOrOptions !== null && !Array.isArray(digitOrOptions) ) { const normalizedOptions = normalizeOptions(digitOrOptions); if (normalizedOptions.status === "failure") { return normalizedOptions; } return formatAwareMask(string, normalizedOptions); } const digit = parseDigit(digitOrOptions); if (digit === null) { return buildFailure("Digit must be a valid integer"); } let legacyOptions = { preserveFormat: false, maskChar: "X", }; if (maybeOptions !== undefined) { const normalizedOptions = normalizeOptions(maybeOptions); if (normalizedOptions.status === "failure") { return normalizedOptions; } legacyOptions = { preserveFormat: normalizedOptions.preserveFormat, maskChar: normalizedOptions.maskChar, alternateMask: normalizedOptions.alternateMask, }; } return legacyMask(string, digit, legacyOptions); } catch (error) { return buildFailure("Something went wrong, Please check the syntax."); } } module.exports = stringMask;