UNPKG

@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
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); };