string-masking
Version:
Mask strings while optionally preserving formatting characters
330 lines (264 loc) • 8.1 kB
JavaScript
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;