135 lines
2.6 KiB
TypeScript
135 lines
2.6 KiB
TypeScript
export type TextSearchOptions = {
|
|
caseSensitive: boolean;
|
|
useRegex: boolean;
|
|
};
|
|
|
|
export type TextSearchMatch = {
|
|
start: number;
|
|
end: number;
|
|
text: string;
|
|
};
|
|
|
|
export type TextSearchResult = {
|
|
matches: TextSearchMatch[];
|
|
error: string;
|
|
};
|
|
|
|
const buildFlags = (options: TextSearchOptions, global = true) => {
|
|
let flags = global ? "g" : "";
|
|
if (!options.caseSensitive) {
|
|
flags += "i";
|
|
}
|
|
return flags;
|
|
};
|
|
|
|
export const escapeSearchKeyword = (value: string) => (
|
|
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
);
|
|
|
|
export const buildSearchRegex = (
|
|
keyword: string,
|
|
options: TextSearchOptions,
|
|
global = true
|
|
) => {
|
|
if (!keyword) {
|
|
return { regex: undefined, error: "" };
|
|
}
|
|
try {
|
|
const source = options.useRegex ? keyword : escapeSearchKeyword(keyword);
|
|
const regex = new RegExp(source, buildFlags(options, global));
|
|
const emptyMatch = "".match(regex);
|
|
if (emptyMatch && emptyMatch[0] === "") {
|
|
return {
|
|
regex: undefined,
|
|
error: "正则不能匹配空文本"
|
|
};
|
|
}
|
|
return { regex, error: "" };
|
|
} catch {
|
|
return {
|
|
regex: undefined,
|
|
error: "正则表达式无效"
|
|
};
|
|
}
|
|
};
|
|
|
|
export const findTextMatches = (
|
|
value: string,
|
|
keyword: string,
|
|
options: TextSearchOptions
|
|
): TextSearchResult => {
|
|
const { regex, error } = buildSearchRegex(keyword, options, true);
|
|
if (!regex || error) {
|
|
return {
|
|
matches: [],
|
|
error
|
|
};
|
|
}
|
|
const matches: TextSearchMatch[] = [];
|
|
let match = regex.exec(value);
|
|
while (match) {
|
|
if (!match[0]) {
|
|
return {
|
|
matches: [],
|
|
error: "正则不能匹配空文本"
|
|
};
|
|
}
|
|
matches.push({
|
|
start: match.index,
|
|
end: match.index + match[0].length,
|
|
text: match[0]
|
|
});
|
|
match = regex.exec(value);
|
|
}
|
|
return {
|
|
matches,
|
|
error: ""
|
|
};
|
|
};
|
|
|
|
export const replaceMatchText = (
|
|
matchText: string,
|
|
keyword: string,
|
|
replaceText: string,
|
|
options: TextSearchOptions
|
|
) => {
|
|
if (!options.useRegex) {
|
|
return replaceText;
|
|
}
|
|
const { regex, error } = buildSearchRegex(keyword, options, false);
|
|
if (!regex || error) {
|
|
return matchText;
|
|
}
|
|
return matchText.replace(regex, replaceText);
|
|
};
|
|
|
|
export const replaceAllText = (
|
|
value: string,
|
|
keyword: string,
|
|
replaceText: string,
|
|
options: TextSearchOptions
|
|
) => {
|
|
const { matches, error } = findTextMatches(value, keyword, options);
|
|
if (error || 1 > matches.length) {
|
|
return {
|
|
value,
|
|
count: 0,
|
|
error
|
|
};
|
|
}
|
|
let nextValue = "";
|
|
let lastEnd = 0;
|
|
matches.forEach((match) => {
|
|
nextValue += value.slice(lastEnd, match.start);
|
|
nextValue += replaceMatchText(match.text, keyword, replaceText, options);
|
|
lastEnd = match.end;
|
|
});
|
|
nextValue += value.slice(lastEnd);
|
|
return {
|
|
value: nextValue,
|
|
count: matches.length,
|
|
error: ""
|
|
};
|
|
};
|
|
|