static-cms/src/lib/regexHelper.js
2017-09-26 14:50:56 -04:00

150 lines
4.7 KiB
JavaScript

import { last } from 'lodash';
/**
* Joins an array of regular expressions into a single expression, without
* altering the received expressions. Only flags passed as an argument will
* apply to the resulting regular expression.
*/
export function joinPatternSegments(patterns, flags = '') {
const pattern = patterns.map(p => p.source).join('');
return new RegExp(pattern, flags);
}
/**
* Combines an array of regular expressions into a single expression, wrapping
* each in a non-capturing group and interposing alternation characters (|) so
* that each expression is executed separately. Only flags passed as an argument
* will apply to the resulting regular expression.
*/
export function combinePatterns(patterns, flags = '') {
const pattern = patterns.map(p => `(?:${p.source})`).join('|');
return new RegExp(pattern, flags);
}
/**
* Modify substrings within a string if they match a (global) pattern. Can be
* inverted to only modify non-matches.
*
* params:
* matchPattern - regexp - a regular expression to check for matches
* replaceFn - function - a replacement function that receives a matched
* substring and returns a replacement substring
* text - string - the string to process
* invertMatchPattern - boolean - if true, non-matching substrings are modified
* instead of matching substrings
*/
export function replaceWhen(matchPattern, replaceFn, text, invertMatchPattern) {
/**
* Splits the string into an array of objects with the following shape:
*
* {
* index: number - the index of the substring within the string
* text: string - the substring
* match: boolean - true if the substring matched `matchPattern`
* }
*
* Loops through matches via recursion (`RegExp.exec` tracks the loop
* internally).
*/
function split(exp, text, acc) {
/**
* Get the next match starting from the end of the last match or start of
* string.
*/
const match = exp.exec(text);
const lastEntry = last(acc);
/**
* `match` will be null if there are no matches.
*/
if (!match) return acc;
/**
* If the match is at the beginning of the input string, normalize to a data
* object with the `match` flag set to `true`, and add to the accumulator.
*/
if (match.index === 0) {
addSubstring(acc, 0, match[0], true);
}
/**
* If there are no entries in the accumulator, convert the substring before
* the match to a data object (without the `match` flag set to true) and
* push to the accumulator, followed by a data object for the matching
* substring.
*/
else if (!lastEntry) {
addSubstring(acc, 0, match.input.slice(0, match.index));
addSubstring(acc, match.index, match[0], true);
}
/**
* If the last entry in the accumulator immediately preceded the current
* matched substring in the original string, just add the data object for
* the matching substring to the accumulator.
*/
else if (match.index === lastEntry.index + lastEntry.text.length) {
addSubstring(acc, match.index, match[0], true);
}
/**
* Convert the substring before the match to a data object (without the
* `match` flag set to true), followed by a data object for the matching
* substring.
*/
else {
const nextIndex = lastEntry.index + lastEntry.text.length;
const nextText = match.input.slice(nextIndex, match.index);
addSubstring(acc, nextIndex, nextText);
addSubstring(acc, match.index, match[0], true);
}
/**
* Continue executing the expression.
*/
return split(exp, text, acc);
}
/**
* Factory for converting substrings to data objects and adding to an output
* array.
*/
function addSubstring(arr, index, text, match = false) {
arr.push({ index, text, match });
}
/**
* Split the input string to an array of data objects, each representing a
* matching or non-matching string.
*/
const acc = split(matchPattern, text, []);
/**
* Process the trailing substring after the final match, if one exists.
*/
const lastEntry = last(acc);
if (!lastEntry) return replaceFn(text);
const nextIndex = lastEntry.index + lastEntry.text.length;
if (text.length > nextIndex) {
acc.push({ index: nextIndex, text: text.slice(nextIndex) });
}
/**
* Map the data objects in the accumulator to their string values, modifying
* matched strings with the replacement function. Modifies non-matches if
* `invertMatchPattern` is truthy.
*/
const replacedText = acc.map(entry => {
const isMatch = invertMatchPattern ? !entry.match : entry.match;
return isMatch ? replaceFn(entry.text) : entry.text;
});
/**
* Return the joined string.
*/
return replacedText.join('');
}