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: "" }; };