@forensic-js/regex
Version:
A module that builds on the existing RegExp module, making it easier working with text matching and replacement both in the browser and node environments
200 lines (159 loc) • 6.44 kB
text/typescript
import {makeArray, isRegex} from '@forensic-js/utils';
export type Pattern = string | RegExp;
export type Callback = (matches: string[], counts: number) => string;
let replacementText = '';
const callback: Callback = (matches: string[], counts: number) => replacementText;
/**
* resolve regex pattern, normalizing the modifiers
*/
const resolveRegexPattern = function(pattern: RegExp): RegExp {
const modifiers = pattern.ignoreCase? 'gmi' : 'gm';
return new RegExp(pattern.source, modifiers);
};
/**
* resolves replaceCount to a number
*/
const resolveReplaceCount = function(replaceCount: boolean | number): number {
if (typeof replaceCount === 'boolean') {
return replaceCount? 1 : -1;
}
else {
return replaceCount;
}
};
/**
* ensure that number of replacement texts at least equals number of patterns
*/
const resolveReplacements = function(patterns: Pattern[], replacements: string []): string[] {
const patternsCount = patterns.length,
replacementsCount = replacements.length,
difference = patternsCount - replacementsCount;
if (difference > 0) {
const fillWith = replacements[replacementsCount - 1];
return [...replacements, ...(Array(difference).fill(fillWith))];
}
else {
return replacements;
}
};
/**
* resolve all capturing groups
*/
const resolveCapturingGroups = (callback: Callback, matches: string[], count: number): string => {
const text = callback(matches, count);
const pattern = resolveRegexPattern(/[$]:(\d+)/);
let result = '';
let length = matches.length;
let startIndex = 0;
let start = -1;
while (++start < length) {
const match = pattern.exec(text);
if (!match) {
break;
}
const index = match.index;
const len = match[0].length;
const current = Number.parseInt(match[1], 10);
const replacementText = current < length? matches[current] : match[0];
result += text.slice(startIndex, index) + replacementText;
startIndex = index + len;
}
result += text.slice(startIndex);
return result;
}
/**
* run replacements using regex pattern as search criteria
*/
const runRegex = function(pattern: RegExp, callback: Callback,
text: string, replaceCount: number): string {
let result: string = '',
start = 0,
counts = 0;
while (++counts && (replaceCount === -1 || counts <= replaceCount)) {
const matches = pattern.exec(text);
if (!matches) {
break;
}
const index = matches.index,
len = matches[0].length;
result += text.slice(start, index) + resolveCapturingGroups(callback, matches, counts);
start = index + len;
}
result += text.slice(start);
return result;
};
/**
* run replacements using string pattern as search criteria
*/
const runString = function(pattern: string, callback: Callback, text: string,
caseSensitive: boolean, replaceCount: number) {
let result: string = '',
start = 0,
counts = 0;
const searchFor = caseSensitive? pattern : pattern.toLowerCase(),
searchFrom = caseSensitive? text : text.toLowerCase(),
len = pattern.length;
while (++counts && (replaceCount === -1 || counts <= replaceCount)) {
const index = searchFrom.indexOf(searchFor, start);
if (index <= -1) {
break;
}
result += text.slice(start, index) + resolveCapturingGroups(callback, [pattern], counts);
start = index + len;
}
result += text.slice(start);
return result;
};
const cordinateReplacement = function(pattern: Pattern, callback: Callback,
text: string, caseSensitive: boolean, replaceCount: number): string {
let result: string = '';
if (isRegex(pattern)) {
result = runRegex(resolveRegexPattern(pattern), callback, text, replaceCount);
}
else {
result = runString(pattern, callback, text, caseSensitive, replaceCount);
}
return result;
};
/**
* iteratively replace every occurence of search patterns with the given replacements text in the
* given text
* @param patterns - search patterns, can be string or regex expressions,
* @param replacements - replacement text for each search pattern
* @param text - the text string to work on
* @param caseSensitive - specifies if search is case sensitive, this only applies to string
* patterns. for regex patterns, use RegExp ignoreCase flag (i)
* @param replaceCount - number of times to replace occurrences for each search pattern. by default
* all occurrences will be replaced. specify true or 1 to replace only first occurrence.
*/
export const replace = (
patterns: Pattern | Pattern[], replacements: string | string[], text: string,
caseSensitive: boolean = false, replaceCount: number | boolean = false): string => {
text = text.toString();
patterns = makeArray(patterns);
replacements = resolveReplacements(patterns, makeArray(replacements));
const count = resolveReplaceCount(replaceCount);
return patterns.reduce<string>((result, pattern, index: number) => {
replacementText = replacements[index];
return cordinateReplacement(pattern, callback, result, caseSensitive, count);
}, text);
};
/**
* iteratively replace every occurence of search patterns with the return value of the
* given callback function in the given text
* @param patterns - search patterns, can be string or regex expressions,
* @param callback - the callback function
* @param text - the text string to work on
* @param caseSensitive - specifies if search is case sensitive, this only applies to string
* patterns. for regex patterns, use RegExp ignoreCase flag (i)
* @param replaceCount - number of times to replace occurrences for each search pattern. by default
* all occurrences will be replaced. specify true or 1 to replace only first occurrence.
*/
export const replaceCallback = (patterns: Pattern | Pattern[], callback: Callback, text: string,
caseSensitive: boolean = false, replaceCount: number | boolean = false): string => {
text = text.toString();
const count = resolveReplaceCount(replaceCount);
return makeArray(patterns).reduce<string>((result, pattern) => {
return cordinateReplacement(pattern, callback, result, caseSensitive, count);
}, text);
};